This commit is contained in:
Manav Rathi
2025-03-12 09:49:28 +05:30
parent 57a226ed2a
commit c07f2f4775
2 changed files with 258 additions and 238 deletions

View File

@@ -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<number>;
/**
* 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<ItemData>) => {
const { timeStampList, columns, shrinkRatio, renderListItem } = data;
return (
<ListItem style={style}>
<ListContainer
gridTemplateColumns={getTemplateColumns(
columns,
shrinkRatio,
timeStampList[index].groups,
)}
>
{renderListItem(timeStampList[index], isScrolling)}
</ListContainer>
</ListItem>
);
},
areEqual,
);
/**
* A virtualized list of files, each represented by their thumbnail.
*/
export const FileList: React.FC<FileListProps> = ({
height,
width,
@@ -1039,3 +894,170 @@ export const FileList: React.FC<FileListProps> = ({
</List>
);
};
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<ItemData>) => {
const { timeStampList, columns, shrinkRatio, renderListItem } = data;
return (
<ListItem style={style}>
<ListContainer
gridTemplateColumns={getTemplateColumns(
columns,
shrinkRatio,
timeStampList[index].groups,
)}
>
{renderListItem(timeStampList[index], isScrolling)}
</ListContainer>
</ListItem>
);
},
areEqual,
);

View File

@@ -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<number>;
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<void>;
};
} & 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<FileListWithViewerProps> = ({
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<FileListWithViewerProps> = ({
<AutoSizer>
{({ height, width }) => (
<FileList
{...{ width, height, annotatedFiles }}
{...{
mode,
modePlus,
showAppDownloadBanner,
selectable,
selected,
setSelected,
activeCollectionID,
activePersonID,
showAppDownloadBanner,
favoriteFileIDs,
}}
{...{ width, height, annotatedFiles }}
onItemClick={handleThumbnailClick}
/>
)}
@@ -266,24 +264,24 @@ export const FileListWithViewer: React.FC<FileListWithViewerProps> = ({
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}
/>
</Container>
);