From c07f2f4775d7fa1bdcf5ac90d4b756051066b5ee Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 12 Mar 2025 09:49:28 +0530 Subject: [PATCH] types --- web/apps/photos/src/components/FileList.tsx | 380 +++++++++--------- .../src/components/FileListWithViewer.tsx | 116 +++--- 2 files changed, 258 insertions(+), 238 deletions(-) diff --git a/web/apps/photos/src/components/FileList.tsx b/web/apps/photos/src/components/FileList.tsx index ba73c43b3b..e9f7260a64 100644 --- a/web/apps/photos/src/components/FileList.tsx +++ b/web/apps/photos/src/components/FileList.tsx @@ -7,9 +7,9 @@ import { IMAGE_CONTAINER_MAX_WIDTH, MIN_COLUMNS, } from "@/new/photos/components/FileList"; +import type { GalleryBarMode } from "@/new/photos/components/gallery/reducer"; import { FlexWrapper } from "@ente/shared/components/Container"; import { Box, Checkbox, Link, Typography, styled } from "@mui/material"; -import type { FileListWithViewerProps } from "components/FileListWithViewer"; import { t } from "i18next"; import memoize from "memoize-one"; import { GalleryContext } from "pages/gallery"; @@ -20,6 +20,7 @@ import { ListChildComponentProps, areEqual, } from "react-window"; +import { SelectedState } from "types/gallery"; import { handleSelectCreator, handleSelectCreatorMulti, @@ -59,126 +60,6 @@ export interface TimeStampListItem { fileCount?: number; } -const ListItem = styled("div")` - display: flex; - justify-content: center; -`; - -const getTemplateColumns = ( - columns: number, - shrinkRatio: number, - groups?: number[], -): string => { - if (groups) { - // need to confirm why this was there - // const sum = groups.reduce((acc, item) => acc + item, 0); - // if (sum < columns) { - // groups[groups.length - 1] += columns - sum; - // } - return groups - .map( - (x) => - `repeat(${x}, ${IMAGE_CONTAINER_MAX_WIDTH * shrinkRatio}px)`, - ) - .join(` ${SPACE_BTW_DATES}px `); - } else { - return `repeat(${columns},${ - IMAGE_CONTAINER_MAX_WIDTH * shrinkRatio - }px)`; - } -}; - -function getFractionFittableColumns(width: number): number { - return ( - (width - 2 * getGapFromScreenEdge(width) + GAP_BTW_TILES) / - (IMAGE_CONTAINER_MAX_WIDTH + GAP_BTW_TILES) - ); -} - -function getGapFromScreenEdge(width: number) { - if (width > MIN_COLUMNS * IMAGE_CONTAINER_MAX_WIDTH) { - return 24; - } else { - return 4; - } -} - -function getShrinkRatio(width: number, columns: number) { - return ( - (width - - 2 * getGapFromScreenEdge(width) - - (columns - 1) * GAP_BTW_TILES) / - (columns * IMAGE_CONTAINER_MAX_WIDTH) - ); -} - -const ListContainer = styled(Box, { - shouldForwardProp: (propName) => propName != "gridTemplateColumns", -})<{ gridTemplateColumns: string }>` - display: grid; - grid-template-columns: ${(props) => props.gridTemplateColumns}; - grid-column-gap: ${GAP_BTW_TILES}px; - width: 100%; - padding: 0 24px; - @media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * MIN_COLUMNS}px) { - padding: 0 4px; - } -`; - -const ListItemContainer = styled(FlexWrapper)<{ span: number }>` - grid-column: span ${(props) => props.span}; -`; - -const DateContainer = styled(ListItemContainer)( - ({ theme }) => ` - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - height: ${DATE_CONTAINER_HEIGHT}px; - color: ${theme.vars.palette.text.muted}; -`, -); - -const FooterContainer = styled(ListItemContainer)` - margin-bottom: 0.75rem; - @media (max-width: 540px) { - font-size: 12px; - margin-bottom: 0.5rem; - } - text-align: center; - justify-content: center; - align-items: flex-end; - margin-top: calc(2rem + 20px); -`; - -const AlbumFooterContainer = styled(ListItemContainer, { - shouldForwardProp: (propName) => propName != "hasReferral", -})<{ hasReferral: boolean }>` - margin-top: 48px; - margin-bottom: ${({ hasReferral }) => (!hasReferral ? `10px` : "0px")}; - text-align: center; - justify-content: center; -`; - -const FullStretchContainer = styled("div")( - ({ theme }) => ` - margin: 0 -24px; - width: calc(100% + 46px); - left: -24px; - @media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * MIN_COLUMNS}px) { - margin: 0 -4px; - width: calc(100% + 6px); - left: -4px; - } - background-color: ${theme.vars.palette.accent.main}; -`, -); - -const NothingContainer = styled(ListItemContainer)` - text-align: center; - justify-content: center; -`; - export interface FileListAnnotatedFile { file: EnteFile; /** @@ -192,21 +73,39 @@ export interface FileListAnnotatedFile { timelineDateString: string; } -type FileListProps = Pick< - FileListWithViewerProps, - | "mode" - | "modePlus" - | "selectable" - | "selected" - | "setSelected" - | "favoriteFileIDs" -> & { +export interface FileListProps { + /** The height we should occupy (needed since the list is virtualized). */ height: number; + /** The width we should occupy.*/ width: number; + /** + * The files to show, annotated with cached precomputed properties that are + * frequently needed by the {@link FileList}. + */ annotatedFiles: FileListAnnotatedFile[]; - showAppDownloadBanner: boolean; + mode?: GalleryBarMode; + /** + * This is an experimental prop, to see if we can merge the separate + * "isInSearchMode" state kept by the gallery to be instead provided as a + * another mode in which the gallery operates. + */ + modePlus?: GalleryBarMode | "search"; + showAppDownloadBanner?: boolean; + selectable?: boolean; + setSelected: ( + selected: SelectedState | ((selected: SelectedState) => SelectedState), + ) => void; + selected: SelectedState; + /** This will be set if mode is not "people". */ activeCollectionID: number; - activePersonID?: string; + /** This will be set if mode is "people". */ + activePersonID?: string | undefined; + /** + * File IDs of all the files that the user has marked as a favorite. + * + * Not set in the context of the shared albums app. + */ + favoriteFileIDs?: Set; /** * Called when the user activates the thumbnail at the given {@link index}. * @@ -214,55 +113,11 @@ type FileListProps = Pick< * {@link annotatedFiles}. */ onItemClick: (index: number) => void; -}; - -interface ItemData { - timeStampList: TimeStampListItem[]; - columns: number; - shrinkRatio: number; - renderListItem: ( - timeStampListItem: TimeStampListItem, - isScrolling?: boolean, - ) => React.JSX.Element; } -const createItemData = memoize( - ( - timeStampList: TimeStampListItem[], - columns: number, - shrinkRatio: number, - renderListItem: ( - timeStampListItem: TimeStampListItem, - isScrolling?: boolean, - ) => React.JSX.Element, - ): ItemData => ({ timeStampList, columns, shrinkRatio, renderListItem }), -); - -const PhotoListRow = React.memo( - ({ - index, - style, - isScrolling, - data, - }: ListChildComponentProps) => { - const { timeStampList, columns, shrinkRatio, renderListItem } = data; - return ( - - - {renderListItem(timeStampList[index], isScrolling)} - - - ); - }, - areEqual, -); - +/** + * A virtualized list of files, each represented by their thumbnail. + */ export const FileList: React.FC = ({ height, width, @@ -1039,3 +894,170 @@ export const FileList: React.FC = ({ ); }; + +const ListItem = styled("div")` + display: flex; + justify-content: center; +`; + +const getTemplateColumns = ( + columns: number, + shrinkRatio: number, + groups?: number[], +): string => { + if (groups) { + // need to confirm why this was there + // const sum = groups.reduce((acc, item) => acc + item, 0); + // if (sum < columns) { + // groups[groups.length - 1] += columns - sum; + // } + return groups + .map( + (x) => + `repeat(${x}, ${IMAGE_CONTAINER_MAX_WIDTH * shrinkRatio}px)`, + ) + .join(` ${SPACE_BTW_DATES}px `); + } else { + return `repeat(${columns},${ + IMAGE_CONTAINER_MAX_WIDTH * shrinkRatio + }px)`; + } +}; + +function getFractionFittableColumns(width: number): number { + return ( + (width - 2 * getGapFromScreenEdge(width) + GAP_BTW_TILES) / + (IMAGE_CONTAINER_MAX_WIDTH + GAP_BTW_TILES) + ); +} + +function getGapFromScreenEdge(width: number) { + if (width > MIN_COLUMNS * IMAGE_CONTAINER_MAX_WIDTH) { + return 24; + } else { + return 4; + } +} + +function getShrinkRatio(width: number, columns: number) { + return ( + (width - + 2 * getGapFromScreenEdge(width) - + (columns - 1) * GAP_BTW_TILES) / + (columns * IMAGE_CONTAINER_MAX_WIDTH) + ); +} + +const ListContainer = styled(Box, { + shouldForwardProp: (propName) => propName != "gridTemplateColumns", +})<{ gridTemplateColumns: string }>` + display: grid; + grid-template-columns: ${(props) => props.gridTemplateColumns}; + grid-column-gap: ${GAP_BTW_TILES}px; + width: 100%; + padding: 0 24px; + @media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * MIN_COLUMNS}px) { + padding: 0 4px; + } +`; + +const ListItemContainer = styled(FlexWrapper)<{ span: number }>` + grid-column: span ${(props) => props.span}; +`; + +const DateContainer = styled(ListItemContainer)( + ({ theme }) => ` + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + height: ${DATE_CONTAINER_HEIGHT}px; + color: ${theme.vars.palette.text.muted}; +`, +); + +const FooterContainer = styled(ListItemContainer)` + margin-bottom: 0.75rem; + @media (max-width: 540px) { + font-size: 12px; + margin-bottom: 0.5rem; + } + text-align: center; + justify-content: center; + align-items: flex-end; + margin-top: calc(2rem + 20px); +`; + +const AlbumFooterContainer = styled(ListItemContainer, { + shouldForwardProp: (propName) => propName != "hasReferral", +})<{ hasReferral: boolean }>` + margin-top: 48px; + margin-bottom: ${({ hasReferral }) => (!hasReferral ? `10px` : "0px")}; + text-align: center; + justify-content: center; +`; + +const FullStretchContainer = styled("div")( + ({ theme }) => ` + margin: 0 -24px; + width: calc(100% + 46px); + left: -24px; + @media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * MIN_COLUMNS}px) { + margin: 0 -4px; + width: calc(100% + 6px); + left: -4px; + } + background-color: ${theme.vars.palette.accent.main}; +`, +); + +const NothingContainer = styled(ListItemContainer)` + text-align: center; + justify-content: center; +`; + +interface ItemData { + timeStampList: TimeStampListItem[]; + columns: number; + shrinkRatio: number; + renderListItem: ( + timeStampListItem: TimeStampListItem, + isScrolling?: boolean, + ) => React.JSX.Element; +} + +const createItemData = memoize( + ( + timeStampList: TimeStampListItem[], + columns: number, + shrinkRatio: number, + renderListItem: ( + timeStampListItem: TimeStampListItem, + isScrolling?: boolean, + ) => React.JSX.Element, + ): ItemData => ({ timeStampList, columns, shrinkRatio, renderListItem }), +); + +const PhotoListRow = React.memo( + ({ + index, + style, + isScrolling, + data, + }: ListChildComponentProps) => { + const { timeStampList, columns, shrinkRatio, renderListItem } = data; + return ( + + + {renderListItem(timeStampList[index], isScrolling)} + + + ); + }, + areEqual, +); diff --git a/web/apps/photos/src/components/FileListWithViewer.tsx b/web/apps/photos/src/components/FileListWithViewer.tsx index baeb7e3775..ccfc3da12d 100644 --- a/web/apps/photos/src/components/FileListWithViewer.tsx +++ b/web/apps/photos/src/components/FileListWithViewer.tsx @@ -1,11 +1,12 @@ import { isSameDay } from "@/base/date"; import { formattedDate } from "@/base/i18n-date"; -import type { FileInfoProps } from "@/gallery/components/FileInfo"; -import { FileViewer } from "@/gallery/components/viewer/FileViewer"; +import { + FileViewer, + type FileViewerProps, +} from "@/gallery/components/viewer/FileViewer"; import { type RenderableSourceURLs } from "@/gallery/services/download"; import type { Collection } from "@/media/collection"; import { EnteFile } from "@/media/file"; -import type { GalleryBarMode } from "@/new/photos/components/gallery/reducer"; import { moveToTrash, TRASH_SECTION } from "@/new/photos/services/collection"; import { styled } from "@mui/material"; import { t } from "i18next"; @@ -17,12 +18,13 @@ import { removeFromFavorites, } from "services/collectionService"; import uploadManager from "services/upload/uploadManager"; -import { - SelectedState, - SetFilesDownloadProgressAttributesCreator, -} from "types/gallery"; +import { SetFilesDownloadProgressAttributesCreator } from "types/gallery"; import { downloadSingleFile } from "utils/file"; -import { FileList, type FileListAnnotatedFile } from "./FileList"; +import { + FileList, + type FileListAnnotatedFile, + type FileListProps, +} from "./FileList"; const Container = styled("div")` display: block; @@ -76,32 +78,12 @@ export type DisplayFile = EnteFile & { timelineDateString?: string; }; -export type FileListWithViewerProps = Pick< - FileInfoProps, - | "fileCollectionIDs" - | "allCollectionsNameByID" - | "onSelectCollection" - | "onSelectPerson" -> & { - mode?: GalleryBarMode; +export type FileListWithViewerProps = { /** - * This is an experimental prop, to see if we can merge the separate - * "isInSearchMode" state kept by the gallery to be instead provided as a - * another mode in which the gallery operates. + * The list of files to show. */ - modePlus?: GalleryBarMode | "search"; files: EnteFile[]; - selectable?: boolean; - setSelected: ( - selected: SelectedState | ((selected: SelectedState) => SelectedState), - ) => void; - selected: SelectedState; - /** - * File IDs of all the files that the user has marked as a favorite. - * - * Not set in the context of the shared albums app. - */ - favoriteFileIDs?: Set; + enableDownload?: boolean; /** * Called when the component wants to update the in-memory, unsynced, * favorite status of a file. @@ -125,45 +107,61 @@ export type FileListWithViewerProps = Pick< * Not set in the context of the shared albums app. */ onMarkTempDeleted?: (files: EnteFile[]) => void; - /** This will be set if mode is not "people". */ - activeCollectionID: number; - /** This will be set if mode is "people". */ - activePersonID?: string | undefined; - enableDownload?: boolean; - showAppDownloadBanner?: boolean; - isInIncomingSharedCollection?: boolean; - isInHiddenSection?: boolean; setFilesDownloadProgressAttributesCreator?: SetFilesDownloadProgressAttributesCreator; /** * Called when the visibility of the file viewer dialog changes. */ onSetOpenFileViewer?: (open: boolean) => void; + /** + * Called when an action in the file viewer requires us to sync with remote. + */ onSyncWithRemote: () => Promise; -}; +} & Pick< + FileListProps, + | "mode" + | "modePlus" + | "showAppDownloadBanner" + | "selectable" + | "selected" + | "setSelected" + | "activeCollectionID" + | "activePersonID" + | "favoriteFileIDs" +> & + Pick< + FileViewerProps, + | "isInIncomingSharedCollection" + | "isInHiddenSection" + | "fileCollectionIDs" + | "allCollectionsNameByID" + | "onSelectCollection" + | "onSelectPerson" + >; /** - * A list of files (represented by their thumbnails), alongwith the viewer that - * opens on activating the thumbnail. + * A list of files (represented by their thumbnails), along with a file viewer + * that opens on activating the thumbnail (and also allows the user to navigate + * through this list of files). */ export const FileListWithViewer: React.FC = ({ mode, modePlus, files, + enableDownload, + showAppDownloadBanner, selectable, selected, setSelected, - favoriteFileIDs, - onMarkUnsyncedFavoriteUpdate, - onMarkTempDeleted, activeCollectionID, activePersonID, - enableDownload, - fileCollectionIDs, - allCollectionsNameByID, - showAppDownloadBanner, + favoriteFileIDs, isInIncomingSharedCollection, isInHiddenSection, + fileCollectionIDs, + allCollectionsNameByID, setFilesDownloadProgressAttributesCreator, + onMarkUnsyncedFavoriteUpdate, + onMarkTempDeleted, onSetOpenFileViewer, onSyncWithRemote, onSelectCollection, @@ -246,18 +244,18 @@ export const FileListWithViewer: React.FC = ({ {({ height, width }) => ( )} @@ -266,24 +264,24 @@ export const FileListWithViewer: React.FC = ({ open={openFileViewer} onClose={handleCloseFileViewer} user={galleryContext.user ?? undefined} - files={files} initialIndex={currentIndex} disableDownload={!enableDownload} - isInIncomingSharedCollection={isInIncomingSharedCollection} isInTrashSection={activeCollectionID === TRASH_SECTION} - isInHiddenSection={isInHiddenSection} - onTriggerSyncWithRemote={handleTriggerSyncWithRemote} - onToggleFavorite={handleToggleFavorite} - onDownload={handleDownload} - onDelete={handleDelete} - onSaveEditedImageCopy={handleSaveEditedImageCopy} {...{ + files, + isInHiddenSection, + isInIncomingSharedCollection, favoriteFileIDs, fileCollectionIDs, allCollectionsNameByID, onSelectCollection, onSelectPerson, }} + onTriggerSyncWithRemote={handleTriggerSyncWithRemote} + onToggleFavorite={handleToggleFavorite} + onDownload={handleDownload} + onDelete={handleDelete} + onSaveEditedImageCopy={handleSaveEditedImageCopy} /> );