From 367d373c183cbb0a3ee574bd001018703f028448 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 12 Mar 2025 09:10:26 +0530 Subject: [PATCH 1/7] Semantic + better layout for long descriptions --- .../gallery/components/viewer/FileViewer.tsx | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index 97c6650fb5..77bf65697f 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -32,7 +32,6 @@ import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined"; import FullscreenExitOutlinedIcon from "@mui/icons-material/FullscreenExitOutlined"; import FullscreenOutlinedIcon from "@mui/icons-material/FullscreenOutlined"; import { - Box, Dialog, DialogContent, DialogTitle, @@ -1025,7 +1024,7 @@ const Shortcuts: React.FC = ({ {t("shortcuts")} - + = ({ ); -const ShortcutsContent = styled(DialogContent)` - display: flex; - flex-direction: column; - gap: 16px; +const ShortcutsContent: React.FC = ({ children }) => ( + + + {children} + + +); + +const ShortcutsTable = styled("table")` + border-collapse: separate; + border-spacing: 0 14px; `; interface ShortcutProps { @@ -1086,12 +1092,18 @@ interface ShortcutProps { } const Shortcut: React.FC = ({ action, shortcut }) => ( - - + + {action} - {shortcut} - + + + {shortcut} + + ); const fileIsEditableImage = (file: EnteFile) => { From 57a226ed2a1e80edcc3e7caa3789e3163e1c49cd Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 12 Mar 2025 09:34:28 +0530 Subject: [PATCH 2/7] Rename --- web/apps/photos/src/components/FileList.tsx | 8 ++++---- .../{PhotoFrame.tsx => FileListWithViewer.tsx} | 11 +++++------ .../src/components/pages/gallery/PreviewCard.tsx | 2 +- web/apps/photos/src/pages/gallery.tsx | 4 ++-- web/apps/photos/src/pages/shared-albums.tsx | 4 ++-- web/packages/new/photos/components/FileList.ts | 2 +- 6 files changed, 15 insertions(+), 16 deletions(-) rename web/apps/photos/src/components/{PhotoFrame.tsx => FileListWithViewer.tsx} (97%) diff --git a/web/apps/photos/src/components/FileList.tsx b/web/apps/photos/src/components/FileList.tsx index 2db2dd196f..ba73c43b3b 100644 --- a/web/apps/photos/src/components/FileList.tsx +++ b/web/apps/photos/src/components/FileList.tsx @@ -9,7 +9,7 @@ import { } from "@/new/photos/components/FileList"; import { FlexWrapper } from "@ente/shared/components/Container"; import { Box, Checkbox, Link, Typography, styled } from "@mui/material"; -import type { PhotoFrameProps } from "components/PhotoFrame"; +import type { FileListWithViewerProps } from "components/FileListWithViewer"; import { t } from "i18next"; import memoize from "memoize-one"; import { GalleryContext } from "pages/gallery"; @@ -193,7 +193,7 @@ export interface FileListAnnotatedFile { } type FileListProps = Pick< - PhotoFrameProps, + FileListWithViewerProps, | "mode" | "modePlus" | "selectable" @@ -545,7 +545,7 @@ export const FileList: React.FC = ({ } else { footerHeight = FOOTER_HEIGHT; } - const photoFrameHeight = (() => { + const fileListHeight = (() => { let sum = 0; const getCurrentItemSize = getItemSize(timeStampList); for (let i = 0; i < timeStampList.length; i++) { @@ -559,7 +559,7 @@ export const FileList: React.FC = ({ return { itemType: ITEM_TYPE.OTHER, item: <>, - height: Math.max(height - photoFrameHeight - footerHeight, 0), + height: Math.max(height - fileListHeight - footerHeight, 0), }; }; diff --git a/web/apps/photos/src/components/PhotoFrame.tsx b/web/apps/photos/src/components/FileListWithViewer.tsx similarity index 97% rename from web/apps/photos/src/components/PhotoFrame.tsx rename to web/apps/photos/src/components/FileListWithViewer.tsx index 806a67ebab..baeb7e3775 100644 --- a/web/apps/photos/src/components/PhotoFrame.tsx +++ b/web/apps/photos/src/components/FileListWithViewer.tsx @@ -76,7 +76,7 @@ export type DisplayFile = EnteFile & { timelineDateString?: string; }; -export type PhotoFrameProps = Pick< +export type FileListWithViewerProps = Pick< FileInfoProps, | "fileCollectionIDs" | "allCollectionsNameByID" @@ -142,9 +142,10 @@ export type PhotoFrameProps = Pick< }; /** - * TODO: Rename me to FileListWithViewer (or Gallery?) + * A list of files (represented by their thumbnails), alongwith the viewer that + * opens on activating the thumbnail. */ -const PhotoFrame = ({ +export const FileListWithViewer: React.FC = ({ mode, modePlus, files, @@ -167,7 +168,7 @@ const PhotoFrame = ({ onSyncWithRemote, onSelectCollection, onSelectPerson, -}: PhotoFrameProps) => { +}) => { const galleryContext = useContext(GalleryContext); const [openFileViewer, setOpenFileViewer] = useState(false); @@ -288,8 +289,6 @@ const PhotoFrame = ({ ); }; -export default PhotoFrame; - /** * See: [Note: Timeline date string] */ diff --git a/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx b/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx index 5b3700971f..0c974ac78a 100644 --- a/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx +++ b/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx @@ -16,7 +16,7 @@ import AlbumOutlinedIcon from "@mui/icons-material/AlbumOutlined"; import FavoriteRoundedIcon from "@mui/icons-material/FavoriteRounded"; import PlayCircleOutlineOutlinedIcon from "@mui/icons-material/PlayCircleOutlineOutlined"; import { styled, Typography } from "@mui/material"; -import type { DisplayFile } from "components/PhotoFrame"; +import type { DisplayFile } from "components/FileListWithViewer"; import { GalleryContext } from "pages/gallery"; import React, { useContext, useEffect, useRef, useState } from "react"; import { shouldShowAvatar } from "utils/file"; diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index 580bc49d58..85d3b59345 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -96,13 +96,13 @@ import CollectionNamer, { import { GalleryBarAndListHeader } from "components/Collections/GalleryBarAndListHeader"; import { Export } from "components/Export"; import { ITEM_TYPE, TimeStampListItem } from "components/FileList"; +import {FileListWithViewer} from "components/FileListWithViewer"; import { FilesDownloadProgress, FilesDownloadProgressAttributes, } from "components/FilesDownloadProgress"; import { FixCreationTime } from "components/FixCreationTime"; import GalleryEmptyState from "components/GalleryEmptyState"; -import PhotoFrame from "components/PhotoFrame"; import { Sidebar } from "components/Sidebar"; import { Upload, type UploadTypeSelectorIntent } from "components/Upload"; import SelectedFileOptions from "components/pages/gallery/SelectedFileOptions"; @@ -1047,7 +1047,7 @@ const Page: React.FC = () => { !state.view.activePerson ? ( ) : ( - - Date: Wed, 12 Mar 2025 09:49:28 +0530 Subject: [PATCH 3/7] 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} /> ); From f92db38ca177d9a67455d5264dbc7ab8fb7664b7 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 12 Mar 2025 10:28:05 +0530 Subject: [PATCH 4/7] uncontext --- .../src/components/FileListWithViewer.tsx | 35 +++++++++---------- web/apps/photos/src/pages/gallery.tsx | 19 +++++----- web/apps/photos/src/pages/shared-albums.tsx | 10 +++--- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/web/apps/photos/src/components/FileListWithViewer.tsx b/web/apps/photos/src/components/FileListWithViewer.tsx index ccfc3da12d..fbf948abb7 100644 --- a/web/apps/photos/src/components/FileListWithViewer.tsx +++ b/web/apps/photos/src/components/FileListWithViewer.tsx @@ -10,8 +10,7 @@ import { EnteFile } from "@/media/file"; import { moveToTrash, TRASH_SECTION } from "@/new/photos/services/collection"; import { styled } from "@mui/material"; import { t } from "i18next"; -import { GalleryContext } from "pages/gallery"; -import { useCallback, useContext, useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import AutoSizer from "react-virtualized-auto-sizer"; import { addToFavorites, @@ -26,19 +25,6 @@ import { type FileListProps, } from "./FileList"; -const Container = styled("div")` - display: block; - flex: 1; - width: 100%; - flex-wrap: wrap; - margin: 0 auto; - overflow: hidden; - .pswp-thumbnail { - display: inline-block; - cursor: pointer; - } -`; - /** * An {@link EnteFile} augmented with various in-memory state used for * displaying it in the photo viewer. @@ -130,6 +116,7 @@ export type FileListWithViewerProps = { > & Pick< FileViewerProps, + | "user" | "isInIncomingSharedCollection" | "isInHiddenSection" | "fileCollectionIDs" @@ -146,6 +133,7 @@ export type FileListWithViewerProps = { export const FileListWithViewer: React.FC = ({ mode, modePlus, + user, files, enableDownload, showAppDownloadBanner, @@ -167,8 +155,6 @@ export const FileListWithViewer: React.FC = ({ onSelectCollection, onSelectPerson, }) => { - const galleryContext = useContext(GalleryContext); - const [openFileViewer, setOpenFileViewer] = useState(false); const [currentIndex, setCurrentIndex] = useState(0); @@ -263,11 +249,11 @@ export const FileListWithViewer: React.FC = ({ = ({ ); }; +const Container = styled("div")` + display: block; + flex: 1; + width: 100%; + flex-wrap: wrap; + margin: 0 auto; + overflow: hidden; + .pswp-thumbnail { + display: inline-block; + cursor: pointer; + } +`; + /** * See: [Note: Timeline date string] */ diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index 85d3b59345..0e0140d5f6 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -96,7 +96,7 @@ import CollectionNamer, { import { GalleryBarAndListHeader } from "components/Collections/GalleryBarAndListHeader"; import { Export } from "components/Export"; import { ITEM_TYPE, TimeStampListItem } from "components/FileList"; -import {FileListWithViewer} from "components/FileListWithViewer"; +import { FileListWithViewer } from "components/FileListWithViewer"; import { FilesDownloadProgress, FilesDownloadProgressAttributes, @@ -1050,18 +1050,19 @@ const Page: React.FC = () => { { "incomingShareViewer" } isInHiddenSection={barMode == "hidden-albums"} + favoriteFileIDs={state.favoriteFileIDs} setFilesDownloadProgressAttributesCreator={ setFilesDownloadProgressAttributesCreator } - selectable={true} onMarkUnsyncedFavoriteUpdate={ handleMarkUnsyncedFavoriteUpdate } diff --git a/web/apps/photos/src/pages/shared-albums.tsx b/web/apps/photos/src/pages/shared-albums.tsx index 7566a280b4..bb887c5977 100644 --- a/web/apps/photos/src/pages/shared-albums.tsx +++ b/web/apps/photos/src/pages/shared-albums.tsx @@ -511,17 +511,17 @@ export default function PublicCollectionGallery() { {blockingLoad && } Date: Wed, 12 Mar 2025 10:30:39 +0530 Subject: [PATCH 5/7] Prune --- web/apps/photos/src/components/FileListWithViewer.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/web/apps/photos/src/components/FileListWithViewer.tsx b/web/apps/photos/src/components/FileListWithViewer.tsx index fbf948abb7..6edec88ab3 100644 --- a/web/apps/photos/src/components/FileListWithViewer.tsx +++ b/web/apps/photos/src/components/FileListWithViewer.tsx @@ -274,16 +274,9 @@ export const FileListWithViewer: React.FC = ({ }; const Container = styled("div")` - display: block; flex: 1; width: 100%; - flex-wrap: wrap; - margin: 0 auto; - overflow: hidden; - .pswp-thumbnail { - display: inline-block; - cursor: pointer; - } + `; /** From 4312b024f681471fb5c319dd9bf35b61c261fae6 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 12 Mar 2025 10:49:23 +0530 Subject: [PATCH 6/7] cleanup --- web/apps/photos/src/components/FileList.tsx | 12 +++- .../src/components/FileListWithViewer.tsx | 41 ------------- .../components/pages/gallery/PreviewCard.tsx | 60 ++++++------------- 3 files changed, 26 insertions(+), 87 deletions(-) diff --git a/web/apps/photos/src/components/FileList.tsx b/web/apps/photos/src/components/FileList.tsx index e9f7260a64..c81da4acb5 100644 --- a/web/apps/photos/src/components/FileList.tsx +++ b/web/apps/photos/src/components/FileList.tsx @@ -66,9 +66,15 @@ export interface FileListAnnotatedFile { * The date string using with the associated {@link file} should be shown in * the timeline. * - * This for used for grouping files: all files which have the same - * {@link timelineDateString} are grouped together into a section titled - * with that {@link timelineDateString}. + * [Note: Timeline date string] + * + * The timeline date string is a formatted date string under which a + * particular file should be grouped in the gallery listing. e.g. "Today", + * "Yesterday", "Fri, 21 Feb" etc. + * + * All files which have the same timelineDateString will be grouped under a + * single section in the gallery listing, prefixed by the timelineDateString + * itself, and a checkbox to select all files on that date. */ timelineDateString: string; } diff --git a/web/apps/photos/src/components/FileListWithViewer.tsx b/web/apps/photos/src/components/FileListWithViewer.tsx index 6edec88ab3..8588b921c6 100644 --- a/web/apps/photos/src/components/FileListWithViewer.tsx +++ b/web/apps/photos/src/components/FileListWithViewer.tsx @@ -4,7 +4,6 @@ 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 { moveToTrash, TRASH_SECTION } from "@/new/photos/services/collection"; @@ -25,45 +24,6 @@ import { type FileListProps, } from "./FileList"; -/** - * An {@link EnteFile} augmented with various in-memory state used for - * displaying it in the photo viewer. - */ -export type DisplayFile = EnteFile & { - src?: string; - srcURLs?: RenderableSourceURLs; - /** - * An object URL corresponding to the image portion, if any, associated with - * the {@link DisplayFile}. - * - * - For images, this will be the object URL of the renderable image itself. - * - For live photos, this will be the object URL of the image portion of - * the live photo. - * - For videos, this will not be defined. - */ - associatedImageURL?: string | undefined; - msrc?: string; - html?: string; - w?: number; - h?: number; - title?: string; - isSourceLoaded?: boolean; - conversionFailed?: boolean; - canForceConvert?: boolean; - /** - * [Note: Timeline date string] - * - * The timeline date string is a formatted date string under which a - * particular file should be grouped in the gallery listing. e.g. "Today", - * "Yesterday", "Fri, 21 Feb" etc. - * - * All files which have the same timelineDateString will be grouped under a - * single section in the gallery listing, prefixed by the timelineDateString - * itself, and a checkbox to select all files on that date. - */ - timelineDateString?: string; -}; - export type FileListWithViewerProps = { /** * The list of files to show. @@ -276,7 +236,6 @@ export const FileListWithViewer: React.FC = ({ const Container = styled("div")` flex: 1; width: 100%; - `; /** diff --git a/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx b/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx index 0c974ac78a..d819390f4b 100644 --- a/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx +++ b/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx @@ -1,8 +1,7 @@ import { Overlay } from "@/base/components/containers"; import { formattedDateRelative } from "@/base/i18n-date"; -import log from "@/base/log"; import { downloadManager } from "@/gallery/services/download"; -import { enteFileDeletionDate } from "@/media/file"; +import { enteFileDeletionDate, type EnteFile } from "@/media/file"; import { FileType } from "@/media/file-type"; import { GAP_BTW_TILES } from "@/new/photos/components/FileList"; import { @@ -16,14 +15,13 @@ import AlbumOutlinedIcon from "@mui/icons-material/AlbumOutlined"; import FavoriteRoundedIcon from "@mui/icons-material/FavoriteRounded"; import PlayCircleOutlineOutlinedIcon from "@mui/icons-material/PlayCircleOutlineOutlined"; import { styled, Typography } from "@mui/material"; -import type { DisplayFile } from "components/FileListWithViewer"; import { GalleryContext } from "pages/gallery"; -import React, { useContext, useEffect, useRef, useState } from "react"; +import React, { useContext, useEffect, useState } from "react"; import { shouldShowAvatar } from "utils/file"; import Avatar from "./Avatar"; interface PreviewCardProps { - file: DisplayFile; + file: EnteFile; onClick: () => void; selectable: boolean; selected: boolean; @@ -216,45 +214,21 @@ export default function PreviewCard({ }: PreviewCardProps) { const galleryContext = useContext(GalleryContext); - const longPressCallback = () => { - onSelect(!selected); - }; + const [imageURL, setImageURL] = useState(undefined); - const longPress = useLongPress(longPressCallback, 500); - - const [imgSrc, setImgSrc] = useState(file.msrc); - - const isMounted = useRef(true); + const longPress = useLongPress(() => onSelect(!selected), 500); useEffect(() => { + let didCancel = false; + + void downloadManager + .renderableThumbnailURL(file, showPlaceholder) + .then((url) => !didCancel && setImageURL(url)); + return () => { - isMounted.current = false; + didCancel = true; }; - }, []); - - useEffect(() => { - const main = async () => { - try { - if (file.msrc) { - return; - } - const url: string = - await downloadManager.renderableThumbnailURL( - file, - showPlaceholder, - ); - - if (!isMounted.current || !url) { - return; - } - setImgSrc(url); - } catch (e) { - log.error("preview card useEffect failed", e); - // no-op - } - }; - main(); - }, [showPlaceholder]); + }, [file, showPlaceholder]); const handleClick = () => { if (selectOnClick) { @@ -263,7 +237,7 @@ export default function PreviewCard({ } else { onSelect(!selected); } - } else if (file?.msrc || imgSrc) { + } else if (imageURL) { onClick?.(); } }; @@ -287,7 +261,7 @@ export default function PreviewCard({ key={`thumb-${file.id}}`} onClick={handleClick} onMouseEnter={handleHover} - disabled={!file?.msrc && !imgSrc} + disabled={!imageURL} {...(selectable ? longPress : {})} > {selectable && ( @@ -301,8 +275,8 @@ export default function PreviewCard({ )} {file.metadata.hasStaticThumbnail ? ( - ) : imgSrc ? ( - + ) : imageURL ? ( + ) : ( )} From c8b07fcae561596516def49d606709fa2e7385af Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 12 Mar 2025 11:01:58 +0530 Subject: [PATCH 7/7] FileThumbnail --- web/apps/photos/src/components/FileList.tsx | 323 +++++++++++++++++- .../components/pages/gallery/PreviewCard.tsx | 323 ------------------ 2 files changed, 320 insertions(+), 326 deletions(-) delete mode 100644 web/apps/photos/src/components/pages/gallery/PreviewCard.tsx diff --git a/web/apps/photos/src/components/FileList.tsx b/web/apps/photos/src/components/FileList.tsx index c81da4acb5..48771df94d 100644 --- a/web/apps/photos/src/components/FileList.tsx +++ b/web/apps/photos/src/components/FileList.tsx @@ -1,6 +1,10 @@ import { assertionFailed } from "@/base/assert"; +import { Overlay } from "@/base/components/containers"; import { isSameDay } from "@/base/date"; -import { EnteFile } from "@/media/file"; +import { formattedDateRelative } from "@/base/i18n-date"; +import { downloadManager } from "@/gallery/services/download"; +import { EnteFile, enteFileDeletionDate } from "@/media/file"; +import { FileType } from "@/media/file-type"; import { GAP_BTW_TILES, IMAGE_CONTAINER_MAX_HEIGHT, @@ -8,8 +12,19 @@ import { MIN_COLUMNS, } from "@/new/photos/components/FileList"; import type { GalleryBarMode } from "@/new/photos/components/gallery/reducer"; +import { + LoadingThumbnail, + StaticThumbnail, +} from "@/new/photos/components/PlaceholderThumbnails"; +import { TileBottomTextOverlay } from "@/new/photos/components/Tiles"; +import { TRASH_SECTION } from "@/new/photos/services/collection"; import { FlexWrapper } from "@ente/shared/components/Container"; +import useLongPress from "@ente/shared/hooks/useLongPress"; +import AlbumOutlinedIcon from "@mui/icons-material/AlbumOutlined"; +import FavoriteRoundedIcon from "@mui/icons-material/FavoriteRounded"; +import PlayCircleOutlineOutlinedIcon from "@mui/icons-material/PlayCircleOutlineOutlined"; import { Box, Checkbox, Link, Typography, styled } from "@mui/material"; +import Avatar from "components/pages/gallery/Avatar"; import { t } from "i18next"; import memoize from "memoize-one"; import { GalleryContext } from "pages/gallery"; @@ -21,12 +36,12 @@ import { areEqual, } from "react-window"; import { SelectedState } from "types/gallery"; +import { shouldShowAvatar } from "utils/file"; import { handleSelectCreator, handleSelectCreatorMulti, } from "utils/photoFrame"; import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery"; -import PreviewCard from "./pages/gallery/PreviewCard"; export const DATE_CONTAINER_HEIGHT = 48; export const SPACE_BTW_DATES = 44; @@ -747,7 +762,7 @@ export const FileList: React.FC = ({ index: number, isScrolling: boolean, ) => ( - onItemClick(index)} @@ -1067,3 +1082,305 @@ const PhotoListRow = React.memo( }, areEqual, ); + +interface FileThumbnailProps { + file: EnteFile; + onClick: () => void; + selectable: boolean; + selected: boolean; + onSelect: (checked: boolean) => void; + onHover: () => void; + onRangeSelect: () => void; + isRangeSelectActive: boolean; + selectOnClick: boolean; + isInsSelectRange: boolean; + activeCollectionID: number; + showPlaceholder: boolean; + isFav: boolean; +} + +const FileThumbnail: React.FC = ({ + file, + onClick, + selectable, + selected, + onSelect, + selectOnClick, + onHover, + onRangeSelect, + isRangeSelectActive, + isInsSelectRange, + isFav, + activeCollectionID, + showPlaceholder, +}) => { + const galleryContext = useContext(GalleryContext); + + const [imageURL, setImageURL] = useState(undefined); + + const longPress = useLongPress(() => onSelect(!selected), 500); + + useEffect(() => { + let didCancel = false; + + void downloadManager + .renderableThumbnailURL(file, showPlaceholder) + .then((url) => !didCancel && setImageURL(url)); + + return () => { + didCancel = true; + }; + }, [file, showPlaceholder]); + + const handleClick = () => { + if (selectOnClick) { + if (isRangeSelectActive) { + onRangeSelect(); + } else { + onSelect(!selected); + } + } else if (imageURL) { + onClick?.(); + } + }; + + const handleSelect: React.ChangeEventHandler = (e) => { + if (isRangeSelectActive) { + onRangeSelect?.(); + } else { + onSelect(e.target.checked); + } + }; + + const handleHover = () => { + if (isRangeSelectActive) { + onHover(); + } + }; + + return ( + + {selectable && ( + e.stopPropagation()} + /> + )} + {file.metadata.hasStaticThumbnail ? ( + + ) : imageURL ? ( + + ) : ( + + )} + {file.metadata.fileType === FileType.livePhoto ? ( + + + + ) : ( + file.metadata.fileType === FileType.video && ( + + + + ) + )} + {selected && } + {shouldShowAvatar(file, galleryContext.user) && ( + + + + )} + {isFav && ( + + + + )} + + + {isRangeSelectActive && isInsSelectRange && ( + + )} + + {activeCollectionID === TRASH_SECTION && file.isTrashed && ( + + + {formattedDateRelative(enteFileDeletionDate(file))} + + + )} + + ); +}; + +const FileThumbnail_ = styled("div")<{ disabled: boolean }>` + display: flex; + width: fit-content; + margin-bottom: ${GAP_BTW_TILES}px; + min-width: 100%; + overflow: hidden; + position: relative; + flex: 1; + cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")}; + user-select: none; + & > img { + object-fit: cover; + max-width: 100%; + min-height: 100%; + flex: 1; + pointer-events: none; + } + + &:hover { + input[type="checkbox"] { + visibility: visible; + opacity: 0.5; + } + + .preview-card-hover-overlay { + opacity: 1; + } + } + + border-radius: 4px; +`; + +const Check = styled("input")<{ $active: boolean }>( + ({ theme, $active }) => ` + appearance: none; + position: absolute; + /* Increase z-index in stacking order to capture clicks */ + z-index: 1; + left: 0; + outline: none; + cursor: pointer; + @media (pointer: coarse) { + pointer-events: none; + } + + &::before { + content: ""; + width: 19px; + height: 19px; + background-color: #ddd; + display: inline-block; + border-radius: 50%; + vertical-align: bottom; + margin: 6px 6px; + transition: background-color 0.3s ease; + pointer-events: inherit; + + } + &::after { + content: ""; + position: absolute; + width: 5px; + height: 11px; + border-right: 2px solid #333; + border-bottom: 2px solid #333; + transition: transform 0.3s ease; + pointer-events: inherit; + transform: translate(-18px, 9px) rotate(45deg); + } + + /* checkmark background (filled circle) */ + &:checked::before { + content: ""; + background-color: ${theme.vars.palette.accent.main}; + border-color: ${theme.vars.palette.accent.main}; + color: white; + } + /* checkmark foreground (tick) */ + &:checked::after { + content: ""; + border-right: 2px solid #ddd; + border-bottom: 2px solid #ddd; + } + visibility: hidden; + ${$active && "visibility: visible; opacity: 0.5;"}; + &:checked { + visibility: visible; + opacity: 1 !important; + } +`, +); + +const HoverOverlay = styled("div")<{ checked: boolean }>` + opacity: 0; + left: 0; + top: 0; + outline: none; + height: 40%; + width: 100%; + position: absolute; + ${(props) => + !props.checked && + "background:linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0))"}; +`; + +/** + * An overlay showing the avatars of the person who shared the item, at the top + * right. + */ +const AvatarOverlay = styled(Overlay)` + display: flex; + justify-content: flex-end; + align-items: flex-start; + padding: 5px; +`; + +/** + * An overlay showing the favorite icon at bottom left. + */ +const FavoriteOverlay = styled(Overlay)` + display: flex; + justify-content: flex-start; + align-items: flex-end; + padding: 5px; + color: white; + opacity: 0.6; +`; + +/** + * An overlay with a gradient, showing the file type indicator (e.g. live photo, + * video) at the bottom right. + */ +const FileTypeIndicatorOverlay = styled(Overlay)` + display: flex; + justify-content: flex-end; + align-items: flex-end; + padding: 5px; + color: white; + background: linear-gradient( + 315deg, + rgba(0 0 0 / 0.14) 0%, + rgba(0 0 0 / 0.05) 30%, + transparent 50% + ); +`; + +const InSelectRangeOverlay = styled(Overlay)( + ({ theme }) => ` + outline: none; + background: ${theme.vars.palette.accent.main}; + opacity: 0.14; +`, +); + +const SelectedOverlay = styled(Overlay)( + ({ theme }) => ` + border: 2px solid ${theme.vars.palette.accent.main}; + border-radius: 4px; +`, +); diff --git a/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx b/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx deleted file mode 100644 index d819390f4b..0000000000 --- a/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx +++ /dev/null @@ -1,323 +0,0 @@ -import { Overlay } from "@/base/components/containers"; -import { formattedDateRelative } from "@/base/i18n-date"; -import { downloadManager } from "@/gallery/services/download"; -import { enteFileDeletionDate, type EnteFile } from "@/media/file"; -import { FileType } from "@/media/file-type"; -import { GAP_BTW_TILES } from "@/new/photos/components/FileList"; -import { - LoadingThumbnail, - StaticThumbnail, -} from "@/new/photos/components/PlaceholderThumbnails"; -import { TileBottomTextOverlay } from "@/new/photos/components/Tiles"; -import { TRASH_SECTION } from "@/new/photos/services/collection"; -import useLongPress from "@ente/shared/hooks/useLongPress"; -import AlbumOutlinedIcon from "@mui/icons-material/AlbumOutlined"; -import FavoriteRoundedIcon from "@mui/icons-material/FavoriteRounded"; -import PlayCircleOutlineOutlinedIcon from "@mui/icons-material/PlayCircleOutlineOutlined"; -import { styled, Typography } from "@mui/material"; -import { GalleryContext } from "pages/gallery"; -import React, { useContext, useEffect, useState } from "react"; -import { shouldShowAvatar } from "utils/file"; -import Avatar from "./Avatar"; - -interface PreviewCardProps { - file: EnteFile; - onClick: () => void; - selectable: boolean; - selected: boolean; - onSelect: (checked: boolean) => void; - onHover: () => void; - onRangeSelect: () => void; - isRangeSelectActive: boolean; - selectOnClick: boolean; - isInsSelectRange: boolean; - activeCollectionID: number; - showPlaceholder: boolean; - isFav: boolean; -} - -const Check = styled("input")<{ $active: boolean }>( - ({ theme, $active }) => ` - appearance: none; - position: absolute; - /* Increase z-index in stacking order to capture clicks */ - z-index: 1; - left: 0; - outline: none; - cursor: pointer; - @media (pointer: coarse) { - pointer-events: none; - } - - &::before { - content: ""; - width: 19px; - height: 19px; - background-color: #ddd; - display: inline-block; - border-radius: 50%; - vertical-align: bottom; - margin: 6px 6px; - transition: background-color 0.3s ease; - pointer-events: inherit; - - } - &::after { - content: ""; - position: absolute; - width: 5px; - height: 11px; - border-right: 2px solid #333; - border-bottom: 2px solid #333; - transition: transform 0.3s ease; - pointer-events: inherit; - transform: translate(-18px, 9px) rotate(45deg); - } - - /* checkmark background (filled circle) */ - &:checked::before { - content: ""; - background-color: ${theme.vars.palette.accent.main}; - border-color: ${theme.vars.palette.accent.main}; - color: white; - } - /* checkmark foreground (tick) */ - &:checked::after { - content: ""; - border-right: 2px solid #ddd; - border-bottom: 2px solid #ddd; - } - visibility: hidden; - ${$active && "visibility: visible; opacity: 0.5;"}; - &:checked { - visibility: visible; - opacity: 1 !important; - } -`, -); - -const HoverOverlay = styled("div")<{ checked: boolean }>` - opacity: 0; - left: 0; - top: 0; - outline: none; - height: 40%; - width: 100%; - position: absolute; - ${(props) => - !props.checked && - "background:linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0))"}; -`; - -/** - * An overlay showing the avatars of the person who shared the item, at the top - * right. - */ -const AvatarOverlay = styled(Overlay)` - display: flex; - justify-content: flex-end; - align-items: flex-start; - padding: 5px; -`; - -/** - * An overlay showing the favorite icon at bottom left. - */ -const FavoriteOverlay = styled(Overlay)` - display: flex; - justify-content: flex-start; - align-items: flex-end; - padding: 5px; - color: white; - opacity: 0.6; -`; - -/** - * An overlay with a gradient, showing the file type indicator (e.g. live photo, - * video) at the bottom right. - */ -const FileTypeIndicatorOverlay = styled(Overlay)` - display: flex; - justify-content: flex-end; - align-items: flex-end; - padding: 5px; - color: white; - background: linear-gradient( - 315deg, - rgba(0 0 0 / 0.14) 0%, - rgba(0 0 0 / 0.05) 30%, - transparent 50% - ); -`; - -const InSelectRangeOverlay = styled(Overlay)( - ({ theme }) => ` - outline: none; - background: ${theme.vars.palette.accent.main}; - opacity: 0.14; -`, -); - -const SelectedOverlay = styled(Overlay)( - ({ theme }) => ` - border: 2px solid ${theme.vars.palette.accent.main}; - border-radius: 4px; -`, -); - -const Cont = styled("div")<{ disabled: boolean }>` - display: flex; - width: fit-content; - margin-bottom: ${GAP_BTW_TILES}px; - min-width: 100%; - overflow: hidden; - position: relative; - flex: 1; - cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")}; - user-select: none; - & > img { - object-fit: cover; - max-width: 100%; - min-height: 100%; - flex: 1; - pointer-events: none; - } - - &:hover { - input[type="checkbox"] { - visibility: visible; - opacity: 0.5; - } - - .preview-card-hover-overlay { - opacity: 1; - } - } - - border-radius: 4px; -`; - -export default function PreviewCard({ - file, - onClick, - selectable, - selected, - onSelect, - selectOnClick, - onHover, - onRangeSelect, - isRangeSelectActive, - isInsSelectRange, - isFav, - activeCollectionID, - showPlaceholder, -}: PreviewCardProps) { - const galleryContext = useContext(GalleryContext); - - const [imageURL, setImageURL] = useState(undefined); - - const longPress = useLongPress(() => onSelect(!selected), 500); - - useEffect(() => { - let didCancel = false; - - void downloadManager - .renderableThumbnailURL(file, showPlaceholder) - .then((url) => !didCancel && setImageURL(url)); - - return () => { - didCancel = true; - }; - }, [file, showPlaceholder]); - - const handleClick = () => { - if (selectOnClick) { - if (isRangeSelectActive) { - onRangeSelect(); - } else { - onSelect(!selected); - } - } else if (imageURL) { - onClick?.(); - } - }; - - const handleSelect: React.ChangeEventHandler = (e) => { - if (isRangeSelectActive) { - onRangeSelect?.(); - } else { - onSelect(e.target.checked); - } - }; - - const handleHover = () => { - if (isRangeSelectActive) { - onHover(); - } - }; - - return ( - - {selectable && ( - e.stopPropagation()} - /> - )} - {file.metadata.hasStaticThumbnail ? ( - - ) : imageURL ? ( - - ) : ( - - )} - {file.metadata.fileType === FileType.livePhoto ? ( - - - - ) : ( - file.metadata.fileType === FileType.video && ( - - - - ) - )} - {selected && } - {shouldShowAvatar(file, galleryContext.user) && ( - - - - )} - {isFav && ( - - - - )} - - - {isRangeSelectActive && isInsSelectRange && ( - - )} - - {activeCollectionID === TRASH_SECTION && file.isTrashed && ( - - - {formattedDateRelative(enteFileDeletionDate(file))} - - - )} - - ); -}