[web] People bar - Part x/x (#3445)

This commit is contained in:
Manav Rathi
2024-09-24 18:30:41 +05:30
committed by GitHub
17 changed files with 415 additions and 355 deletions

View File

@@ -1,34 +0,0 @@
import { AllCollectionTile } from "@/new/photos/components/ItemCards";
import type { CollectionSummary } from "@/new/photos/types/collection";
import { Typography } from "@mui/material";
import { t } from "i18next";
import CollectionCard from "../CollectionCard";
import { AllCollectionTileText } from "../styledComponents";
interface Iprops {
collectionSummary: CollectionSummary;
onCollectionClick: (collectionID: number) => void;
isScrolling?: boolean;
}
export default function AllCollectionCard({
onCollectionClick,
collectionSummary,
isScrolling,
}: Iprops) {
return (
<CollectionCard
collectionTile={AllCollectionTile}
coverFile={collectionSummary.coverFile}
onClick={() => onCollectionClick(collectionSummary.id)}
isScrolling={isScrolling}
>
<AllCollectionTileText>
<Typography>{collectionSummary.name}</Typography>
<Typography variant="small" color="text.muted">
{t("photos_count", { count: collectionSummary.fileCount })}
</Typography>
</AllCollectionTileText>
</CollectionCard>
);
}

View File

@@ -1,7 +1,13 @@
import {
AllCollectionTile,
ItemCard,
LargeTileTextOverlay,
} from "@/new/photos/components/ItemCards";
import type { CollectionSummary } from "@/new/photos/types/collection";
import { FlexWrapper } from "@ente/shared/components/Container";
import useWindowSize from "@ente/shared/hooks/useWindowSize";
import { DialogContent } from "@mui/material";
import { DialogContent, Typography } from "@mui/material";
import { t } from "i18next";
import memoize from "memoize-one";
import React, { useEffect, useRef, useState } from "react";
import {
@@ -9,13 +15,13 @@ import {
ListChildComponentProps,
areEqual,
} from "react-window";
import AllCollectionCard from "./collectionCard";
import { AllCollectionMobileBreakpoint } from "./dialog";
const MobileColumns = 2;
const DesktopColumns = 3;
const CollectionRowItemSize = 154;
const getCollectionRowListHeight = (
collectionRowList: CollectionSummary[][],
windowSize: { height: number; width: number },
@@ -147,3 +153,29 @@ export default function AllCollectionContent({
</DialogContent>
);
}
interface AllCollectionCardProps {
collectionSummary: CollectionSummary;
onCollectionClick: (collectionID: number) => void;
isScrolling?: boolean;
}
const AllCollectionCard: React.FC<AllCollectionCardProps> = ({
onCollectionClick,
collectionSummary,
isScrolling,
}) => (
<ItemCard
TileComponent={AllCollectionTile}
coverFile={collectionSummary.coverFile}
onClick={() => onCollectionClick(collectionSummary.id)}
isScrolling={isScrolling}
>
<LargeTileTextOverlay>
<Typography>{collectionSummary.name}</Typography>
<Typography variant="small" color="text.muted">
{t("photos_count", { count: collectionSummary.fileCount })}
</Typography>
</LargeTileTextOverlay>
</ItemCard>
);

View File

@@ -1,55 +0,0 @@
import {
LoadingThumbnail,
StaticThumbnail,
} from "@/new/photos/components/PlaceholderThumbnails";
import downloadManager from "@/new/photos/services/download";
import { EnteFile } from "@/new/photos/types/file";
import { useEffect, useState } from "react";
/** Deprecated in favor of {@link ItemCard}. */
export default function CollectionCard(props: {
children?: any;
coverFile: EnteFile;
onClick: () => void;
collectionTile: any;
isScrolling?: boolean;
}) {
const {
coverFile: file,
onClick,
children,
collectionTile: CustomCollectionTile,
isScrolling,
} = props;
const [coverImageURL, setCoverImageURL] = useState(null);
useEffect(() => {
const main = async () => {
if (!file) {
return;
}
const url = await downloadManager.getThumbnailForPreview(
file,
isScrolling,
);
if (url) {
setCoverImageURL(url);
}
};
main();
}, [file, isScrolling]);
return (
<CustomCollectionTile onClick={onClick}>
{file?.metadata.hasStaticThumbnail ? (
<StaticThumbnail fileType={file?.metadata.fileType} />
) : coverImageURL ? (
<img src={coverImageURL} />
) : (
<LoadingThumbnail />
)}
{children}
</CustomCollectionTile>
);
}

View File

@@ -6,7 +6,7 @@ import {
GalleryItemsHeaderAdapter,
GalleryItemsSummary,
} from "@/new/photos/components/Gallery/ListHeader";
import { SpaceBetweenBox } from "@/new/photos/components/mui-custom";
import { SpaceBetweenFlex } from "@/new/photos/components/mui-custom";
import type {
CollectionSummary,
CollectionSummaryType,
@@ -101,7 +101,7 @@ export const CollectionHeader: React.FC<CollectionHeaderProps> = ({
return (
<GalleryItemsHeaderAdapter>
<SpaceBetweenBox>
<SpaceBetweenFlex>
<GalleryItemsSummary
name={name}
fileCount={fileCount}
@@ -110,7 +110,7 @@ export const CollectionHeader: React.FC<CollectionHeaderProps> = ({
{shouldShowOptions(type) && (
<CollectionOptions collectionSummaryType={type} {...rest} />
)}
</SpaceBetweenBox>
</SpaceBetweenFlex>
</GalleryItemsHeaderAdapter>
);
};

View File

@@ -1,11 +1,22 @@
import type { Collection } from "@/media/collection";
import {
AllCollectionTile,
ItemCard,
ItemTileOverlay,
LargeTileTextOverlay,
} from "@/new/photos/components/ItemCards";
import type {
CollectionSummaries,
CollectionSummary,
} from "@/new/photos/types/collection";
import { FlexWrapper } from "@ente/shared/components/Container";
import DialogTitleWithCloseButton from "@ente/shared/components/DialogBox/TitleWithCloseButton";
import { DialogContent, useMediaQuery } from "@mui/material";
import {
DialogContent,
styled,
Typography,
useMediaQuery,
} from "@mui/material";
import { AllCollectionDialog } from "components/Collections/AllCollections/dialog";
import { t } from "i18next";
import { useEffect, useState } from "react";
@@ -17,8 +28,6 @@ import {
isAddToAllowedCollection,
isMoveToAllowedCollection,
} from "utils/collection";
import AddCollectionButton from "./AddCollectionButton";
import CollectionSelectorCard from "./CollectionCard";
export interface CollectionSelectorAttributes {
callback: (collection: Collection) => void;
@@ -28,19 +37,20 @@ export interface CollectionSelectorAttributes {
onCancel?: () => void;
}
interface Props {
interface CollectionSelectorProps {
open: boolean;
onClose: () => void;
attributes: CollectionSelectorAttributes;
collections: Collection[];
collectionSummaries: CollectionSummaries;
}
function CollectionSelector({
export const CollectionSelector: React.FC<CollectionSelectorProps> = ({
attributes,
collectionSummaries,
collections,
...props
}: Props) {
}) => {
const isMobile = useMediaQuery("(max-width: 428px)");
const [collectionsToShow, setCollectionsToShow] = useState<
@@ -146,15 +156,53 @@ function CollectionSelector({
/>
{collectionsToShow.map((collectionSummary) => (
<CollectionSelectorCard
onCollectionClick={handleCollectionClick}
collectionSummary={collectionSummary}
key={collectionSummary.id}
collectionSummary={collectionSummary}
onCollectionClick={handleCollectionClick}
/>
))}
</FlexWrapper>
</DialogContent>
</AllCollectionDialog>
);
};
interface CollectionSelectorCardProps {
collectionSummary: CollectionSummary;
onCollectionClick: (collectionID: number) => void;
}
export default CollectionSelector;
const CollectionSelectorCard: React.FC<CollectionSelectorCardProps> = ({
collectionSummary,
onCollectionClick,
}) => (
<ItemCard
TileComponent={AllCollectionTile}
coverFile={collectionSummary.coverFile}
onClick={() => onCollectionClick(collectionSummary.id)}
>
<LargeTileTextOverlay>
<Typography>{collectionSummary.name}</Typography>
</LargeTileTextOverlay>
</ItemCard>
);
interface AddCollectionButtonProps {
showNextModal: () => void;
}
const AddCollectionButton: React.FC<AddCollectionButtonProps> = ({
showNextModal,
}) => (
<ItemCard TileComponent={AllCollectionTile} onClick={showNextModal}>
<LargeTileTextOverlay>{t("create_albums")}</LargeTileTextOverlay>
<ImageContainer>+</ImageContainer>
</ItemCard>
);
const ImageContainer = styled(ItemTileOverlay)`
display: flex;
justify-content: center;
align-items: center;
font-size: 42px;
`;

View File

@@ -1,30 +0,0 @@
import { AllCollectionTile } from "@/new/photos/components/ItemCards";
import { CenteredFlex, Overlay } from "@ente/shared/components/Container";
import { styled } from "@mui/material";
import CollectionCard from "components/Collections/CollectionCard";
import { AllCollectionTileText } from "components/Collections/styledComponents";
import { t } from "i18next";
const ImageContainer = styled(Overlay)`
display: flex;
font-size: 42px;
`;
interface Iprops {
showNextModal: () => void;
}
export default function AddCollectionButton({ showNextModal }: Iprops) {
return (
<CollectionCard
collectionTile={AllCollectionTile}
onClick={() => showNextModal()}
coverFile={null}
>
<AllCollectionTileText>{t("create_albums")}</AllCollectionTileText>
<ImageContainer>
<CenteredFlex>+</CenteredFlex>
</ImageContainer>
</CollectionCard>
);
}

View File

@@ -1,27 +0,0 @@
import { AllCollectionTile } from "@/new/photos/components/ItemCards";
import type { CollectionSummary } from "@/new/photos/types/collection";
import { Typography } from "@mui/material";
import CollectionCard from "../CollectionCard";
import { AllCollectionTileText } from "../styledComponents";
interface Iprops {
collectionSummary: CollectionSummary;
onCollectionClick: (collectionID: number) => void;
}
export default function CollectionSelectorCard({
onCollectionClick,
collectionSummary,
}: Iprops) {
return (
<CollectionCard
collectionTile={AllCollectionTile}
coverFile={collectionSummary.coverFile}
onClick={() => onCollectionClick(collectionSummary.id)}
>
<AllCollectionTileText>
<Typography>{collectionSummary.name}</Typography>
</AllCollectionTileText>
</CollectionCard>
);
}

View File

@@ -1,4 +1,5 @@
import type { Collection } from "@/media/collection";
import { PersonListHeader } from "@/new/photos/components/Gallery";
import {
GalleryBarImpl,
type GalleryBarImplProps,
@@ -8,6 +9,7 @@ import {
type CollectionsSortBy,
type CollectionSummaries,
} from "@/new/photos/types/collection";
import { ensure } from "@/utils/ensure";
import { includes } from "@/utils/type-guards";
import {
getData,
@@ -59,14 +61,24 @@ type CollectionsProps = Omit<
};
/**
* The horizontally scrollable bar shown at the top of the gallery.
* The gallery bar, the header for the list items, and state for any associated
* dialogs that might be triggered by actions on either the bar or the header..
*
* This component includes both the actual bar, and also the surrounding chrome
* and state for any associated dialogs that might be triggered by actions on
* the bar.
* This component manages the sticky horizontally scrollable bar shown at the
* top of the gallery, AND the non-sticky header shown below the bar, at the top
* of the actual list of items.
*
* These are disparate views - indeed, the list header is not even a child of
* this component but is instead proxied via {@link setPhotoListHeader}. Still,
* having this intermediate wrapper component allows us to move some of the
* common concerns shared by both the gallery bar and list header (e.g. some
* dialogs that can be invoked from both places) into this file instead of
* cluttering the already big gallery component.
*
* TODO: Once the gallery code is better responsibilitied out, consider moving
* this code back inline into the gallery.
*/
// TODO-Cluster Rename me to GalleryBar and subsume GalleryBarImpl
export const Collections: React.FC<CollectionsProps> = ({
export const GalleryBarAndListHeader: React.FC<CollectionsProps> = ({
shouldHide,
mode,
onChangeMode,
@@ -132,22 +144,27 @@ export const Collections: React.FC<CollectionsProps> = ({
if (shouldHide) return;
setPhotoListHeader({
item: (
<CollectionHeader
{...{
activeCollection,
setActiveCollectionID,
setCollectionNamerAttributes,
setFilesDownloadProgressAttributesCreator,
isActiveCollectionDownloadInProgress,
}}
collectionSummary={toShowCollectionSummaries.get(
activeCollectionID,
)}
onCollectionShare={() => setOpenCollectionShareView(true)}
onCollectionCast={() => setOpenAlbumCastDialog(true)}
/>
),
item:
mode != "people" ? (
<CollectionHeader
{...{
activeCollection,
setActiveCollectionID,
setCollectionNamerAttributes,
setFilesDownloadProgressAttributesCreator,
isActiveCollectionDownloadInProgress,
}}
collectionSummary={toShowCollectionSummaries.get(
activeCollectionID,
)}
onCollectionShare={() =>
setOpenCollectionShareView(true)
}
onCollectionCast={() => setOpenAlbumCastDialog(true)}
/>
) : (
<PersonListHeader person={ensure(activePerson)} />
),
itemType: ITEM_TYPE.HEADER,
height: 68,
});

View File

@@ -1,20 +0,0 @@
import { Overlay } from "@ente/shared/components/Container";
import { styled } from "@mui/material";
export const ScrollContainer = styled("div")`
width: 100%;
height: 120px;
overflow: auto;
scroll-behavior: smooth;
display: flex;
gap: 4px;
`;
export const AllCollectionTileText = styled(Overlay)`
padding: 8px;
background: linear-gradient(
0deg,
rgba(0, 0, 0, 0.1) 0%,
rgba(0, 0, 0, 0.5) 86.46%
);
`;

View File

@@ -1,11 +1,10 @@
import { PreviewItemTile } from "@/new/photos/components/ItemCards";
import { ItemCard, PreviewItemTile } from "@/new/photos/components/ItemCards";
import { EnteFile } from "@/new/photos/types/file";
import { FlexWrapper } from "@ente/shared/components/Container";
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
import { Box, styled } from "@mui/material";
import ItemList from "components/ItemList";
import { t } from "i18next";
import CollectionCard from "./Collections/CollectionCard";
interface Iprops {
isOpen: boolean;
@@ -29,11 +28,10 @@ const ExportPendingList = (props: Iprops) => {
return (
<FlexWrapper>
<Box sx={{ marginRight: "8px" }}>
<CollectionCard
<ItemCard
key={file.id}
TileComponent={PreviewItemTile}
coverFile={file}
onClick={() => null}
collectionTile={PreviewItemTile}
/>
</Box>
<ItemContainer>
@@ -59,6 +57,7 @@ const ExportPendingList = (props: Iprops) => {
<DialogBoxV2
open={props.isOpen}
onClose={props.onClose}
fullWidth
PaperProps={{
sx: { maxWidth: "444px" },
}}

View File

@@ -3,11 +3,8 @@ import { NavbarBase } from "@/base/components/Navbar";
import { useIsMobileWidth } from "@/base/hooks";
import log from "@/base/log";
import type { Collection } from "@/media/collection";
import { SearchResultsHeader } from "@/new/photos/components/Gallery";
import type { GalleryBarMode } from "@/new/photos/components/Gallery/BarImpl";
import {
GalleryItemsHeaderAdapter,
GalleryItemsSummary,
} from "@/new/photos/components/Gallery/ListHeader";
import {
SearchBar,
type SearchBarProps,
@@ -59,15 +56,16 @@ import ArrowBack from "@mui/icons-material/ArrowBack";
import FileUploadOutlinedIcon from "@mui/icons-material/FileUploadOutlined";
import MenuIcon from "@mui/icons-material/Menu";
import type { ButtonProps, IconButtonProps } from "@mui/material";
import { Box, Button, IconButton, Typography, styled } from "@mui/material";
import { Box, Button, IconButton, Typography } from "@mui/material";
import AuthenticateUserModal from "components/AuthenticateUserModal";
import { Collections } from "components/Collections";
import CollectionNamer, {
CollectionNamerAttributes,
} from "components/Collections/CollectionNamer";
import CollectionSelector, {
import {
CollectionSelector,
CollectionSelectorAttributes,
} from "components/Collections/CollectionSelector";
import { GalleryBarAndListHeader } from "components/Collections/GalleryBarAndListHeader";
import ExportModal from "components/ExportModal";
import {
FilesDownloadProgress,
@@ -150,17 +148,6 @@ import { isArchivedFile } from "utils/magicMetadata";
import { getSessionExpiredMessage } from "utils/ui";
import { getLocalFamilyData } from "utils/user/family";
const SYNC_INTERVAL_IN_MICROSECONDS = 1000 * 60 * 5; // 5 minutes
export const DeadCenter = styled("div")`
flex: 1;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
flex-direction: column;
`;
const defaultGalleryContext: GalleryContextType = {
showPlanSelectorModal: () => null,
setActiveCollectionID: () => null,
@@ -183,8 +170,20 @@ export const GalleryContext = createContext<GalleryContextType>(
defaultGalleryContext,
);
/**
* The default view for logged in users.
*
* I heard you like ascii art.
*
* Navbar / Search ^
* --------------------- |
* Gallery Bar sticky
* --------------------- ---/---
* Photo List Header scrollable
* --------------------- |
* Photo List v
*/
export default function Gallery() {
const router = useRouter();
const [user, setUser] = useState(null);
const [familyData, setFamilyData] = useState<FamilyData>(null);
const [collections, setCollections] = useState<Collection[]>(null);
@@ -297,8 +296,6 @@ export default function Gallery() {
const closeSidebar = () => setSidebarView(false);
const openSidebar = () => setSidebarView(true);
const [photoListHeader, setPhotoListHeader] =
useState<TimeStampListItem>(null);
const [exportModalView, setExportModalView] = useState(false);
@@ -322,7 +319,7 @@ export default function Gallery() {
SearchOption | undefined
>();
// If visible, what should the gallery bar show.
// If visible, what should the (sticky) gallery bar show.
const [barMode, setBarMode] = useState<GalleryBarMode>("albums");
// The currently selected person in the gallery bar (if any).
@@ -330,13 +327,19 @@ export default function Gallery() {
const people = useSyncExternalStore(peopleSubscribe, peopleSnapshot);
const [isClipSearchResult, setIsClipSearchResult] =
useState<boolean>(false);
// The (non-sticky) header shown at the top of the gallery items.
const [photoListHeader, setPhotoListHeader] =
useState<TimeStampListItem>(null);
const [
filesDownloadProgressAttributesList,
setFilesDownloadProgressAttributesList,
] = useState<FilesDownloadProgressAttributes[]>([]);
const [isClipSearchResult, setIsClipSearchResult] =
useState<boolean>(false);
const router = useRouter();
// Ensure that the keys in local storage are not malformed by verifying that
// the recoveryKey can be decrypted with the masterKey.
@@ -398,9 +401,10 @@ export default function Gallery() {
await syncWithRemote(true);
setIsFirstLoad(false);
setJustSignedUp(false);
syncInterval.current = setInterval(() => {
syncWithRemote(false, true);
}, SYNC_INTERVAL_IN_MICROSECONDS);
syncInterval.current = setInterval(
() => syncWithRemote(false, true),
5 * 60 * 1000 /* 5 minutes */,
);
if (electron) {
electron.onMainWindowFocus(() => syncWithRemote(false, true));
if (await shouldShowWhatsNew(electron)) setOpenWhatsNew(true);
@@ -823,10 +827,6 @@ export default function Gallery() {
setHiddenCollectionSummaries(hiddenCollectionSummaries);
};
if (!collectionSummaries || !filteredData) {
return <div />;
}
const setFilesDownloadProgressAttributesCreator: SetFilesDownloadProgressAttributesCreator =
(folderName, collectionID, isHidden) => {
const id = filesDownloadProgressAttributesList?.length ?? 0;
@@ -1037,6 +1037,10 @@ export default function Gallery() {
log.debug(() => ["person", activePerson]);
}
if (!collectionSummaries || !filteredData) {
return <div></div>;
}
return (
<GalleryContext.Provider
value={{
@@ -1126,7 +1130,7 @@ export default function Gallery() {
)}
</NavbarBase>
<Collections
<GalleryBarAndListHeader
{...{
shouldHide: isInSearchMode,
mode: barMode,
@@ -1365,21 +1369,3 @@ const HiddenSectionNavbarContents: React.FC<
</FlexWrapper>
</HorizontalFlex>
);
interface SearchResultsHeaderProps {
selectedOption: SearchOption;
}
const SearchResultsHeader: React.FC<SearchResultsHeaderProps> = ({
selectedOption,
}) => (
<GalleryItemsHeaderAdapter>
<Typography color="text.muted" variant="large">
{t("search_results")}
</Typography>
<GalleryItemsSummary
name={selectedOption.suggestion.label}
fileCount={selectedOption.fileCount}
/>
</GalleryItemsHeaderAdapter>
);

View File

@@ -7,7 +7,7 @@ import {
GalleryItemsHeaderAdapter,
GalleryItemsSummary,
} from "@/new/photos/components/Gallery/ListHeader";
import { SpaceBetweenBox } from "@/new/photos/components/mui-custom";
import { SpaceBetweenFlex } from "@/new/photos/components/mui-custom";
import downloadManager from "@/new/photos/services/download";
import { EnteFile } from "@/new/photos/types/file";
import { mergeMetadata } from "@/new/photos/utils/file";
@@ -275,58 +275,21 @@ export default function PublicCollectionGallery() {
main();
}, []);
const downloadEnabled = useMemo(
() => publicCollection?.publicURLs?.[0]?.enableDownload ?? true,
[publicCollection],
);
const downloadAllFiles = async () => {
try {
if (!downloadEnabled) {
return;
}
const setFilesDownloadProgressAttributes =
setFilesDownloadProgressAttributesCreator(
publicCollection.name,
publicCollection.id,
isHiddenCollection(publicCollection),
);
await downloadCollectionFiles(
publicCollection.name,
publicFiles,
setFilesDownloadProgressAttributes,
);
} catch (e) {
log.error("failed to downloads shared album all files", e);
}
};
const downloadEnabled =
publicCollection?.publicURLs?.[0]?.enableDownload ?? true;
useEffect(() => {
publicCollection &&
publicFiles &&
setPhotoListHeader({
item: (
<GalleryItemsHeaderAdapter>
<SpaceBetweenBox>
<GalleryItemsSummary
name={publicCollection.name}
fileCount={publicFiles.length}
/>
{downloadEnabled && (
<OverflowMenu
ariaControls={"collection-options"}
triggerButtonIcon={<MoreHoriz />}
>
<OverflowMenuOption
startIcon={<FileDownloadOutlinedIcon />}
onClick={downloadAllFiles}
>
{t("download_album")}
</OverflowMenuOption>
</OverflowMenu>
)}
</SpaceBetweenBox>
</GalleryItemsHeaderAdapter>
<ListHeader
{...{
publicCollection,
publicFiles,
setFilesDownloadProgressAttributesCreator,
}}
/>
),
itemType: ITEM_TYPE.HEADER,
height: 68,
@@ -733,3 +696,56 @@ const SelectedFileOptions: React.FC<SelectedFileOptionsProps> = ({
</SelectionBar>
);
};
interface ListHeaderProps {
publicCollection: Collection;
publicFiles: EnteFile[];
setFilesDownloadProgressAttributesCreator: SetFilesDownloadProgressAttributesCreator;
}
const ListHeader: React.FC<ListHeaderProps> = ({
publicCollection,
publicFiles,
setFilesDownloadProgressAttributesCreator,
}) => {
const downloadEnabled =
publicCollection.publicURLs?.[0]?.enableDownload ?? true;
const downloadAllFiles = async () => {
const setFilesDownloadProgressAttributes =
setFilesDownloadProgressAttributesCreator(
publicCollection.name,
publicCollection.id,
isHiddenCollection(publicCollection),
);
await downloadCollectionFiles(
publicCollection.name,
publicFiles,
setFilesDownloadProgressAttributes,
);
};
return (
<GalleryItemsHeaderAdapter>
<SpaceBetweenFlex>
<GalleryItemsSummary
name={publicCollection.name}
fileCount={publicFiles.length}
/>
{downloadEnabled && (
<OverflowMenu
ariaControls={"collection-options"}
triggerButtonIcon={<MoreHoriz />}
>
<OverflowMenuOption
startIcon={<FileDownloadOutlinedIcon />}
onClick={downloadAllFiles}
>
{t("download_album")}
</OverflowMenuOption>
</OverflowMenu>
)}
</SpaceBetweenFlex>
</GalleryItemsHeaderAdapter>
);
};

View File

@@ -1,6 +1,10 @@
import { useIsMobileWidth } from "@/base/hooks";
import { CollectionsSortOptions } from "@/new/photos/components/CollectionsSortOptions";
import { BarItemTile, ItemCard } from "@/new/photos/components/ItemCards";
import {
BarItemTile,
ItemCard,
TileTextOverlay,
} from "@/new/photos/components/ItemCards";
import {
FilledIconButton,
UnstyledButton,
@@ -498,20 +502,11 @@ interface CardTextProps {
}
const CardText: React.FC<CardTextProps> = ({ text }) => (
<CardText_>
<TileTextOverlay>
<TruncatedText {...{ text }} />
</CardText_>
</TileTextOverlay>
);
const CardText_ = styled(Overlay)`
padding: 4px;
background: linear-gradient(
0deg,
rgba(0, 0, 0, 0.1) 0%,
rgba(0, 0, 0, 0.5) 86.46%
);
`;
const TruncatedText: React.FC<CardTextProps> = ({ text }) => (
<Tooltip title={text}>
<Box height={"2.1em"} overflow="hidden">

View File

@@ -1,13 +1,30 @@
import { Box, Stack, styled, Typography } from "@mui/material";
import {
Box,
Stack,
styled,
Typography,
type TypographyProps,
} from "@mui/material";
import { t } from "i18next";
import React from "react";
interface GalleryItemsSummaryProps {
/** The name / title for the items that are being shown. */
/**
* The name / title for the items that are being shown.
*/
name: string;
/** The number of items being shown. */
/**
* Optional extra props to pass to the {@link Typography} component that
* shows {@link name}
*/
nameProps?: TypographyProps;
/**
* The number of items being shown.
*/
fileCount: number;
/** An element (usually an icon) placed after the file count. */
/**
* An optional element, usually an icon, placed after the file count.
*/
endIcon?: React.ReactNode;
}
@@ -17,35 +34,34 @@ interface GalleryItemsSummaryProps {
*/
export const GalleryItemsSummary: React.FC<GalleryItemsSummaryProps> = ({
name,
nameProps,
fileCount,
endIcon,
}) => {
return (
<div>
<Typography variant="h3">{name}</Typography>
}) => (
<div>
<Typography variant="h3" {...(nameProps ?? {})}>
{name}
</Typography>
<Stack
direction="row"
gap={1.5}
sx={{
// Keep height the same even when there is no endIcon
minHeight: "24px",
}}
>
<Typography variant="small" color="text.muted">
{t("photos_count", { count: fileCount })}
</Typography>
{endIcon && (
<Box
sx={{ svg: { fontSize: "17px", color: "text.muted" } }}
>
{endIcon}
</Box>
)}
</Stack>
</div>
);
};
<Stack
direction="row"
gap={1.5}
sx={{
// Keep height the same even when there is no endIcon
minHeight: "24px",
}}
>
<Typography variant="small" color="text.muted">
{t("photos_count", { count: fileCount })}
</Typography>
{endIcon && (
<Box sx={{ svg: { fontSize: "17px", color: "text.muted" } }}>
{endIcon}
</Box>
)}
</Stack>
</div>
);
/**
* A component suitable for wrapping a component which is acting like a gallery

View File

@@ -0,0 +1,72 @@
/**
* @file code that really belongs to pages/gallery.tsx itself (or related
* files), but it written here in a separate file so that we can write in this
* package that has TypeScript strict mode enabled.
*
* Once the original gallery.tsx is strict mode, this code can be inlined back
* there.
*/
import type { Person } from "@/new/photos/services/ml/cgroups";
import type { SearchOption } from "@/new/photos/services/search/types";
import OverflowMenu from "@ente/shared/components/OverflowMenu/menu";
import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option";
import EditIcon from "@mui/icons-material/Edit";
import MoreHoriz from "@mui/icons-material/MoreHoriz";
import { Typography } from "@mui/material";
import { t } from "i18next";
import React from "react";
import { SpaceBetweenFlex } from "../mui-custom";
import { GalleryItemsHeaderAdapter, GalleryItemsSummary } from "./ListHeader";
interface SearchResultsHeaderProps {
selectedOption: SearchOption;
}
export const SearchResultsHeader: React.FC<SearchResultsHeaderProps> = ({
selectedOption,
}) => (
<GalleryItemsHeaderAdapter>
<Typography color="text.muted" variant="large">
{t("search_results")}
</Typography>
<GalleryItemsSummary
name={selectedOption.suggestion.label}
fileCount={selectedOption.fileCount}
/>
</GalleryItemsHeaderAdapter>
);
interface PeopleListHeaderProps {
person: Person;
}
export const PersonListHeader: React.FC<PeopleListHeaderProps> = ({
person,
}) => {
const hasOptions = process.env.NEXT_PUBLIC_ENTE_WIP_CL;
return (
<GalleryItemsHeaderAdapter>
<SpaceBetweenFlex>
<GalleryItemsSummary
name={person.name ?? "Unnamed person"}
nameProps={person.name ? {} : { color: "text.muted" }}
fileCount={person.fileIDs.length}
/>
{hasOptions && (
<OverflowMenu
ariaControls={"person-options"}
triggerButtonIcon={<MoreHoriz />}
>
<OverflowMenuOption
startIcon={<EditIcon />}
onClick={() => console.log("test")}
>
{t("download_album")}
</OverflowMenuOption>
</OverflowMenu>
)}
</SpaceBetweenFlex>
</GalleryItemsHeaderAdapter>
);
};

View File

@@ -8,12 +8,14 @@ import { styled } from "@mui/material";
import React, { useEffect, useState } from "react";
interface ItemCardProps {
/** One of the *Tile components to use as the top level element. */
/**
* One of the *Tile components to use as the top level element.
*/
TileComponent: React.FC<React.PropsWithChildren>;
/**
* The file (if any) whose thumbnail (if any) should be should be shown.
* Optional file whose thumbnail (if any) should be should be shown.
*/
coverFile: EnteFile | undefined;
coverFile?: EnteFile | undefined;
/**
* Optional boolean indicating if the user is currently scrolling.
*
@@ -21,14 +23,14 @@ interface ItemCardProps {
* downloads.
*/
isScrolling?: boolean;
/** Optional click handler. */
/**
* Optional click handler.
*/
onClick?: () => void;
}
/**
* A generic card that can be be used to represent collections, files, people -
* anything that has an associated "cover photo".
*
* This is a simplified variant / almost-duplicate of {@link CollectionCard}.
* A generic card that can be be used to represent collections, files, people -
* anything that (usually) has an associated "cover photo".
*/
export const ItemCard: React.FC<React.PropsWithChildren<ItemCardProps>> = ({
TileComponent,
@@ -63,9 +65,13 @@ export const ItemCard: React.FC<React.PropsWithChildren<ItemCardProps>> = ({
/**
* A generic "base" tile, meant to be used (after setting dimensions) as the
* {@link TileComponent} provided to an {@link ItemCard}.
*
* Use {@link ItemTileOverlay} (usually via one of its presets) to overlay
* content on top of the tile.
*/
export const ItemTile = styled("div")`
display: flex;
/* Act as container for the absolutely positioned ItemTileOverlays. */
position: relative;
border-radius: 4px;
overflow: hidden;
@@ -104,3 +110,42 @@ export const AllCollectionTile = styled(ItemTile)`
width: 150px;
height: 150px;
`;
/**
* An empty overlay on top of the nearest relative positioned ancestor.
*
* This is meant to be used in tandem with {@link ItemTile}.
*/
export const ItemTileOverlay = styled("div")`
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
`;
/**
* An {@link ItemTileOverlay} suitable for hosting textual content for small and
* medium sized tiles.
*/
export const TileTextOverlay = styled(ItemTileOverlay)`
padding: 4px;
background: linear-gradient(
0deg,
rgba(0, 0, 0, 0.1) 0%,
rgba(0, 0, 0, 0.5) 86.46%
);
`;
/**
* A variation of {@link TileTextOverlay} for use with larger tiles like the
* {@link AllCollectionTile}.
*/
export const LargeTileTextOverlay = styled(ItemTileOverlay)`
padding: 8px;
background: linear-gradient(
0deg,
rgba(0, 0, 0, 0.1) 0%,
rgba(0, 0, 0, 0.5) 86.46%
);
`;

View File

@@ -40,7 +40,7 @@ export const UnstyledButton = styled("button")`
* and its uses moved to this one when possible (so that we can then see where
* the width: 100% is essential).
*/
export const SpaceBetweenBox = styled(Box)`
export const SpaceBetweenFlex = styled(Box)`
display: flex;
justify-content: space-between;
align-items: center;