[web] People bar - Part x/x (#3445)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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%
|
||||
);
|
||||
`;
|
||||
@@ -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" },
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
72
web/packages/new/photos/components/Gallery/index.tsx
Normal file
72
web/packages/new/photos/components/Gallery/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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%
|
||||
);
|
||||
`;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user