[web] Collection selector related cleanup - Part 2/2 (#3585)

Completes https://github.com/ente-io/ente/pull/3581
This commit is contained in:
Manav Rathi
2024-10-05 18:31:21 +05:30
committed by GitHub
17 changed files with 404 additions and 361 deletions

View File

@@ -1,8 +1,8 @@
import {
AllCollectionTile,
CollectionTileButton,
ItemCard,
LargeTileTextOverlay,
} from "@/new/photos/components/ItemCards";
} from "@/new/photos/components/Tiles";
import type { CollectionSummary } from "@/new/photos/services/collection/ui";
import { FlexWrapper } from "@ente/shared/components/Container";
import useWindowSize from "@ente/shared/hooks/useWindowSize";
@@ -68,7 +68,7 @@ const AllCollectionRow = React.memo(
<div style={style}>
<FlexWrapper gap={"4px"} padding={"16px"}>
{collectionRow.map((item: any) => (
<AllCollectionCard
<CollectionButton
isScrolling={isScrolling}
onCollectionClick={onCollectionClick}
collectionSummary={item}
@@ -160,13 +160,13 @@ interface AllCollectionCardProps {
isScrolling?: boolean;
}
const AllCollectionCard: React.FC<AllCollectionCardProps> = ({
const CollectionButton: React.FC<AllCollectionCardProps> = ({
onCollectionClick,
collectionSummary,
isScrolling,
}) => (
<ItemCard
TileComponent={AllCollectionTile}
TileComponent={CollectionTileButton}
coverFile={collectionSummary.coverFile}
onClick={() => onCollectionClick(collectionSummary.id)}
isScrolling={isScrolling}

View File

@@ -1,232 +0,0 @@
import type { Collection } from "@/media/collection";
import {
AllCollectionTile,
ItemCard,
ItemTileOverlay,
LargeTileTextOverlay,
} from "@/new/photos/components/ItemCards";
import {
canAddToCollection,
canMoveToCollection,
CollectionSummaryOrder,
type CollectionSummaries,
type CollectionSummary,
} from "@/new/photos/services/collection/ui";
import { FlexWrapper } from "@ente/shared/components/Container";
import DialogTitleWithCloseButton from "@ente/shared/components/DialogBox/TitleWithCloseButton";
import {
Dialog,
DialogContent,
styled,
Typography,
useMediaQuery,
} from "@mui/material";
import { t } from "i18next";
import { useEffect, useState } from "react";
export enum CollectionSelectorIntent {
upload,
add,
move,
restore,
unhide,
}
export interface CollectionSelectorAttributes {
callback: (collection: Collection) => void;
showNextModal: () => void;
/**
* The {@link intent} modifies the title of the dialog, and also filters
* the list of collections the user can select from appropriately.
*/
intent: CollectionSelectorIntent;
fromCollection?: number;
onCancel?: () => void;
}
interface CollectionSelectorProps {
open: boolean;
onClose: () => void;
attributes: CollectionSelectorAttributes;
collectionSummaries: CollectionSummaries;
/**
* A function to map from a collection ID to a {@link Collection}.
*
* This is invoked when the user makes a selection, to convert the ID of the
* selected collection into a collection object that can be passed to the
* {@link callback} attribute of {@link CollectionSelectorAttributes}.
*/
collectionForCollectionID: (collectionID: number) => Promise<Collection>;
}
/**
* A dialog allowing the user to select one of their existing collections or
* create a new one.
*/
export const CollectionSelector: React.FC<CollectionSelectorProps> = ({
attributes,
collectionSummaries,
collectionForCollectionID,
...props
}) => {
// Make the dialog fullscreen if the screen is <= the dialog's max width.
const isFullScreen = useMediaQuery("(max-width: 494px)");
const [collectionsToShow, setCollectionsToShow] = useState<
CollectionSummary[]
>([]);
useEffect(() => {
if (!attributes || !props.open) {
return;
}
const main = async () => {
const collectionsToShow = [...collectionSummaries.values()]
?.filter(({ id, type }) => {
if (id === attributes.fromCollection) {
return false;
} else if (
attributes.intent === CollectionSelectorIntent.add
) {
return canAddToCollection(type);
} else if (
attributes.intent === CollectionSelectorIntent.upload
) {
return (
canMoveToCollection(type) || type == "uncategorized"
);
} else if (
attributes.intent === CollectionSelectorIntent.restore
) {
return (
canMoveToCollection(type) || type == "uncategorized"
);
} else {
return canMoveToCollection(type);
}
})
.sort((a, b) => {
return a.name.localeCompare(b.name);
})
.sort((a, b) => {
return (
CollectionSummaryOrder.get(a.type) -
CollectionSummaryOrder.get(b.type)
);
});
if (collectionsToShow.length === 0) {
props.onClose();
attributes.showNextModal();
}
setCollectionsToShow(collectionsToShow);
};
main();
}, [collectionSummaries, attributes, props.open]);
if (!collectionsToShow?.length) {
return <></>;
}
const handleCollectionClick = async (collectionID: number) => {
attributes.callback(await collectionForCollectionID(collectionID));
props.onClose();
};
const onUserTriggeredClose = () => {
attributes.onCancel?.();
props.onClose();
};
return (
<Dialog_
onClose={onUserTriggeredClose}
open={props.open}
fullScreen={isFullScreen}
fullWidth
>
<DialogTitleWithCloseButton onClose={onUserTriggeredClose}>
{attributes.intent === CollectionSelectorIntent.upload
? t("upload_to_album")
: attributes.intent === CollectionSelectorIntent.add
? t("add_to_album")
: attributes.intent === CollectionSelectorIntent.move
? t("move_to_album")
: attributes.intent === CollectionSelectorIntent.restore
? t("restore_to_album")
: attributes.intent ===
CollectionSelectorIntent.unhide
? t("unhide_to_album")
: t("select_album")}
</DialogTitleWithCloseButton>
<DialogContent sx={{ "&&&": { padding: 0 } }}>
<FlexWrapper flexWrap="wrap" gap={"4px"} padding={"16px"}>
<AddCollectionButton
showNextModal={attributes.showNextModal}
/>
{collectionsToShow.map((collectionSummary) => (
<CollectionSelectorCard
key={collectionSummary.id}
collectionSummary={collectionSummary}
onCollectionClick={handleCollectionClick}
/>
))}
</FlexWrapper>
</DialogContent>
</Dialog_>
);
};
export const AllCollectionMobileBreakpoint = 559;
export const Dialog_ = styled(Dialog)(({ theme }) => ({
"& .MuiPaper-root": {
maxWidth: "494px",
},
"& .MuiDialogTitle-root": {
padding: "16px",
paddingRight: theme.spacing(1),
},
"& .MuiDialogContent-root": {
padding: "16px",
},
}));
interface CollectionSelectorCardProps {
collectionSummary: CollectionSummary;
onCollectionClick: (collectionID: number) => void;
}
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

@@ -5,8 +5,8 @@ import {
} from "@/new/photos/components/Gallery/BarImpl";
import { PeopleHeader } from "@/new/photos/components/Gallery/PeopleHeader";
import {
areOnlySystemCollections,
collectionsSortBy,
hasNonSystemCollections,
isSystemCollection,
shouldShowOnCollectionBar,
type CollectionsSortBy,
@@ -122,7 +122,7 @@ export const GalleryBarAndListHeader: React.FC<CollectionsProps> = ({
const shouldBeHidden = useMemo(
() =>
shouldHide ||
(!hasNonSystemCollections(toShowCollectionSummaries) &&
(areOnlySystemCollections(toShowCollectionSummaries) &&
activeCollectionID === ALL_SECTION),
[shouldHide, toShowCollectionSummaries, activeCollectionID],
);

View File

@@ -1,4 +1,4 @@
import { ItemCard, PreviewItemTile } from "@/new/photos/components/ItemCards";
import { ItemCard, PreviewItemTile } from "@/new/photos/components/Tiles";
import { EnteFile } from "@/new/photos/types/file";
import { FlexWrapper } from "@ente/shared/components/Container";
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";

View File

@@ -3,6 +3,7 @@ import log from "@/base/log";
import type { CollectionMapping, Electron, ZipItem } from "@/base/types/ipc";
import type { Collection } from "@/media/collection";
import { CollectionMappingChoiceDialog } from "@/new/photos/components/CollectionMappingChoiceDialog";
import type { CollectionSelectorAttributes } from "@/new/photos/components/CollectionSelector";
import { exportMetadataDirectoryName } from "@/new/photos/services/export";
import type {
FileAndPath,
@@ -13,7 +14,6 @@ import { firstNonEmpty } from "@/utils/array";
import { ensure } from "@/utils/ensure";
import { CustomError } from "@ente/shared/error";
import DiscFullIcon from "@mui/icons-material/DiscFull";
import { CollectionSelectorIntent } from "components/Collections/CollectionSelector";
import UserNameInputDialog from "components/UserNameInputDialog";
import { t } from "i18next";
import isElectron from "is-electron";
@@ -36,12 +36,7 @@ import type {
} from "services/upload/uploadManager";
import uploadManager from "services/upload/uploadManager";
import watcher from "services/watch";
import {
SetCollectionSelectorAttributes,
SetCollections,
SetFiles,
SetLoading,
} from "types/gallery";
import { SetCollections, SetFiles, SetLoading } from "types/gallery";
import { NotificationAttributes } from "types/Notification";
import { getOrCreateAlbum } from "utils/collection";
import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
@@ -64,9 +59,17 @@ enum PICKED_UPLOAD_TYPE {
interface Props {
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
closeCollectionSelector?: () => void;
closeUploadTypeSelector: () => void;
setCollectionSelectorAttributes?: SetCollectionSelectorAttributes;
/**
* Show the collection selector with the given {@link attributes}.
*/
onOpenCollectionSelector?: (
attributes: CollectionSelectorAttributes,
) => void;
/**
* Close the collection selector if it is open.
*/
onCloseCollectionSelector?: () => void;
setCollectionNamerAttributes?: SetCollectionNamerAttributes;
setLoading: SetLoading;
setShouldDisableDropzone: (value: boolean) => void;
@@ -460,17 +463,17 @@ export default function Uploader({
showCollectionCreateModal(importSuggestion.rootFolderName);
}
props.setCollectionSelectorAttributes({
callback: uploadFilesToExistingCollection,
props.onOpenCollectionSelector({
action: "upload",
onSelectCollection: uploadFilesToExistingCollection,
onCreateCollection: showNextModal,
onCancel: handleCollectionSelectorCancel,
showNextModal,
intent: CollectionSelectorIntent.upload,
});
})();
}, [webFiles, desktopFiles, desktopFilePaths, desktopZipItems]);
const preCollectionCreationAction = async () => {
props.closeCollectionSelector?.();
props.onCloseCollectionSelector?.();
props.setShouldDisableDropzone(!uploadManager.shouldAllowNewUpload());
setUploadStage(UPLOAD_STAGES.START);
setUploadProgressView(true);

View File

@@ -1,5 +1,6 @@
import { SelectionBar } from "@/base/components/Navbar";
import type { Collection } from "@/media/collection";
import type { CollectionSelectorAttributes } from "@/new/photos/components/CollectionSelector";
import type { GalleryBarMode } from "@/new/photos/components/Gallery/BarImpl";
import { FluidContainer } from "@ente/shared/components/Container";
import ClockIcon from "@mui/icons-material/AccessTime";
@@ -15,11 +16,9 @@ import UnArchiveIcon from "@mui/icons-material/Unarchive";
import VisibilityOffOutlined from "@mui/icons-material/VisibilityOffOutlined";
import VisibilityOutlined from "@mui/icons-material/VisibilityOutlined";
import { Box, IconButton, Stack, Tooltip } from "@mui/material";
import { CollectionSelectorIntent } from "components/Collections/CollectionSelector";
import { t } from "i18next";
import { AppContext } from "pages/_app";
import { useContext } from "react";
import { SetCollectionSelectorAttributes } from "types/gallery";
import {
ALL_SECTION,
ARCHIVE_SECTION,
@@ -36,7 +35,15 @@ interface Props {
) => (...args: any[]) => void;
handleFileOps: (opsType: FILE_OPS_TYPE) => (...args: any[]) => void;
showCreateCollectionModal: (opsType: COLLECTION_OPS_TYPE) => () => void;
setCollectionSelectorAttributes: SetCollectionSelectorAttributes;
/**
* Callback to open a dialog where the user can choose a collection.
*
* The reason for opening the dialog and other properties are passed as the
* {@link attributes} argument.
*/
onOpenCollectionSelector: (
attributes: CollectionSelectorAttributes,
) => void;
count: number;
ownCount: number;
clearSelection: () => void;
@@ -52,7 +59,7 @@ interface Props {
const SelectedFileOptions = ({
showCreateCollectionModal,
setCollectionSelectorAttributes,
onOpenCollectionSelector,
handleCollectionOps,
handleFileOps,
selectedCollection,
@@ -72,12 +79,14 @@ const SelectedFileOptions = ({
const peopleMode = barMode == "people";
const addToCollection = () =>
setCollectionSelectorAttributes({
callback: handleCollectionOps(COLLECTION_OPS_TYPE.ADD),
showNextModal: showCreateCollectionModal(COLLECTION_OPS_TYPE.ADD),
intent: CollectionSelectorIntent.add,
fromCollection:
!isInSearchMode && !peopleMode ? activeCollectionID : undefined,
onOpenCollectionSelector({
action: "add",
onSelectCollection: handleCollectionOps(COLLECTION_OPS_TYPE.ADD),
onCreateCollection: showCreateCollectionModal(
COLLECTION_OPS_TYPE.ADD,
),
ignoredCollectionID:
isInSearchMode || peopleMode ? undefined : activeCollectionID,
});
const trashHandler = () =>
@@ -98,12 +107,14 @@ const SelectedFileOptions = ({
});
const restoreHandler = () =>
setCollectionSelectorAttributes({
callback: handleCollectionOps(COLLECTION_OPS_TYPE.RESTORE),
showNextModal: showCreateCollectionModal(
onOpenCollectionSelector({
action: "restore",
onSelectCollection: handleCollectionOps(
COLLECTION_OPS_TYPE.RESTORE,
),
onCreateCollection: showCreateCollectionModal(
COLLECTION_OPS_TYPE.RESTORE,
),
intent: CollectionSelectorIntent.restore,
});
const removeFromCollectionHandler = () => {
@@ -141,22 +152,24 @@ const SelectedFileOptions = ({
};
const moveToCollection = () => {
setCollectionSelectorAttributes({
callback: handleCollectionOps(COLLECTION_OPS_TYPE.MOVE),
showNextModal: showCreateCollectionModal(COLLECTION_OPS_TYPE.MOVE),
intent: CollectionSelectorIntent.move,
fromCollection:
!isInSearchMode && !peopleMode ? activeCollectionID : undefined,
onOpenCollectionSelector({
action: "move",
onSelectCollection: handleCollectionOps(COLLECTION_OPS_TYPE.MOVE),
onCreateCollection: showCreateCollectionModal(
COLLECTION_OPS_TYPE.MOVE,
),
ignoredCollectionID:
isInSearchMode || peopleMode ? undefined : activeCollectionID,
});
};
const unhideToCollection = () => {
setCollectionSelectorAttributes({
callback: handleCollectionOps(COLLECTION_OPS_TYPE.UNHIDE),
showNextModal: showCreateCollectionModal(
onOpenCollectionSelector({
action: "unhide",
onSelectCollection: handleCollectionOps(COLLECTION_OPS_TYPE.UNHIDE),
onCreateCollection: showCreateCollectionModal(
COLLECTION_OPS_TYPE.UNHIDE,
),
intent: CollectionSelectorIntent.unhide,
});
};

View File

@@ -3,6 +3,10 @@ import { NavbarBase } from "@/base/components/Navbar";
import { useIsMobileWidth } from "@/base/hooks";
import log from "@/base/log";
import type { Collection } from "@/media/collection";
import {
CollectionSelector,
type CollectionSelectorAttributes,
} from "@/new/photos/components/CollectionSelector";
import {
PeopleEmptyState,
SearchResultsHeader,
@@ -16,7 +20,7 @@ import {
import { WhatsNew } from "@/new/photos/components/WhatsNew";
import { shouldShowWhatsNew } from "@/new/photos/services/changelog";
import type { CollectionSummaries } from "@/new/photos/services/collection/ui";
import { hasNonSystemCollections } from "@/new/photos/services/collection/ui";
import { areOnlySystemCollections } from "@/new/photos/services/collection/ui";
import downloadManager from "@/new/photos/services/download";
import {
getLocalFiles,
@@ -67,10 +71,6 @@ import AuthenticateUserModal from "components/AuthenticateUserModal";
import CollectionNamer, {
CollectionNamerAttributes,
} from "components/Collections/CollectionNamer";
import {
CollectionSelector,
CollectionSelectorAttributes,
} from "components/Collections/CollectionSelector";
import { GalleryBarAndListHeader } from "components/Collections/GalleryBarAndListHeader";
import ExportModal from "components/ExportModal";
import {
@@ -96,6 +96,7 @@ import { useRouter } from "next/router";
import { AppContext } from "pages/_app";
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
@@ -212,9 +213,6 @@ export default function Gallery() {
});
const [planModalView, setPlanModalView] = useState(false);
const [blockingLoad, setBlockingLoad] = useState(false);
const [collectionSelectorAttributes, setCollectionSelectorAttributes] =
useState<CollectionSelectorAttributes>(null);
const [collectionSelectorView, setCollectionSelectorView] = useState(false);
const [collectionNamerAttributes, setCollectionNamerAttributes] =
useState<CollectionNamerAttributes>(null);
const [collectionNamerView, setCollectionNamerView] = useState(false);
@@ -350,6 +348,10 @@ export default function Gallery() {
new Set<number>(),
);
const [openCollectionSelector, setOpenCollectionSelector] = useState(false);
const [collectionSelectorAttributes, setCollectionSelectorAttributes] =
useState<CollectionSelectorAttributes | undefined>();
const router = useRouter();
// Ensure that the keys in local storage are not malformed by verifying that
@@ -474,10 +476,6 @@ export default function Gallery() {
setEmailList(emailList);
}, [user, collections, familyData]);
useEffect(() => {
collectionSelectorAttributes && setCollectionSelectorView(true);
}, [collectionSelectorAttributes]);
useEffect(() => {
collectionNamerAttributes && setCollectionNamerView(true);
}, [collectionNamerAttributes]);
@@ -700,7 +698,7 @@ export default function Gallery() {
if (
sidebarView ||
uploadTypeSelectorView ||
collectionSelectorView ||
openCollectionSelector ||
collectionNamerView ||
fixCreationTimeView ||
planModalView ||
@@ -943,7 +941,7 @@ export default function Gallery() {
(ops: COLLECTION_OPS_TYPE) => async (collection: Collection) => {
startLoading();
try {
setCollectionSelectorView(false);
setOpenCollectionSelector(false);
const selectedFiles = getSelectedFiles(selected, filteredData);
const toProcessFiles =
ops === COLLECTION_OPS_TYPE.REMOVE
@@ -1067,10 +1065,6 @@ export default function Gallery() {
setUploadTypeSelectorIntent(intent ?? "upload");
};
const closeCollectionSelector = () => {
setCollectionSelectorView(false);
};
const openExportModal = () => {
setExportModalView(true);
};
@@ -1112,6 +1106,19 @@ export default function Gallery() {
setBarMode("people");
};
const handleOpenCollectionSelector = useCallback(
(attributes: CollectionSelectorAttributes) => {
setCollectionSelectorAttributes(attributes);
setOpenCollectionSelector(true);
},
[],
);
const handleCloseCollectionSelector = useCallback(
() => setOpenCollectionSelector(false),
[],
);
if (!collectionSummaries || !filteredData) {
return <div></div>;
}
@@ -1173,10 +1180,10 @@ export default function Gallery() {
attributes={collectionNamerAttributes}
/>
<CollectionSelector
open={collectionSelectorView}
onClose={closeCollectionSelector}
collectionSummaries={collectionSummaries}
open={openCollectionSelector}
onClose={handleCloseCollectionSelector}
attributes={collectionSelectorAttributes}
collectionSummaries={collectionSummaries}
collectionForCollectionID={(id) =>
findCollectionCreatingUncategorizedIfNeeded(
collections,
@@ -1244,29 +1251,20 @@ export default function Gallery() {
<Uploader
activeCollection={activeCollection}
syncWithRemote={syncWithRemote}
showCollectionSelector={setCollectionSelectorView.bind(
null,
true,
)}
closeUploadTypeSelector={setUploadTypeSelectorView.bind(
null,
false,
)}
setCollectionSelectorAttributes={
setCollectionSelectorAttributes
}
closeCollectionSelector={setCollectionSelectorView.bind(
null,
false,
)}
onOpenCollectionSelector={handleOpenCollectionSelector}
onCloseCollectionSelector={handleCloseCollectionSelector}
setLoading={setBlockingLoad}
setCollectionNamerAttributes={setCollectionNamerAttributes}
setShouldDisableDropzone={setShouldDisableDropzone}
setFiles={setFiles}
setCollections={setCollections}
isFirstUpload={
!hasNonSystemCollections(collectionSummaries)
}
isFirstUpload={areOnlySystemCollections(
collectionSummaries,
)}
{...{
dragAndDropFiles,
openFileSelector,
@@ -1336,8 +1334,8 @@ export default function Gallery() {
showCreateCollectionModal={
showCreateCollectionModal
}
setCollectionSelectorAttributes={
setCollectionSelectorAttributes
onOpenCollectionSelector={
handleOpenCollectionSelector
}
count={selected.count}
ownCount={selected.ownCount}

View File

@@ -2,7 +2,6 @@ import type { Collection } from "@/media/collection";
import { type SelectionContext } from "@/new/photos/components/Gallery";
import { EnteFile } from "@/new/photos/types/file";
import type { User } from "@ente/shared/user/types";
import { CollectionSelectorAttributes } from "components/Collections/CollectionSelector";
import { FilesDownloadProgressAttributes } from "components/FilesDownloadProgress";
import { TimeStampListItem } from "components/PhotoList";
@@ -24,9 +23,6 @@ export type SetSelectedState = React.Dispatch<
export type SetFiles = React.Dispatch<React.SetStateAction<EnteFile[]>>;
export type SetCollections = React.Dispatch<React.SetStateAction<Collection[]>>;
export type SetLoading = React.Dispatch<React.SetStateAction<boolean>>;
export type SetCollectionSelectorAttributes = React.Dispatch<
React.SetStateAction<CollectionSelectorAttributes>
>;
export type SetFilesDownloadProgressAttributes = (
value:
| Partial<FilesDownloadProgressAttributes>

View File

@@ -303,7 +303,6 @@
"LARGER_THAN_AVAILABLE_STORAGE_INFO": "These files were not uploaded as they exceed the maximum size limit for your storage plan",
"TOO_LARGE_INFO": "These files were not uploaded as they exceed our maximum file size limit",
"THUMBNAIL_GENERATION_FAILED_INFO": "These files were uploaded, but unfortunately we could not generate the thumbnails for them.",
"select_album": "Select album",
"upload_to_album": "Upload to album",
"add_to_album": "Add to album",
"move_to_album": "Move to album",

View File

@@ -12,9 +12,12 @@ import {
import { t } from "i18next";
import React from "react";
import { SpaceBetweenFlex } from "./mui";
import { DialogCloseIconButton, type DialogVisiblityProps } from "./mui/Dialog";
import {
DialogCloseIconButton,
type DialogVisibilityProps,
} from "./mui/Dialog";
type CollectionMappingChoiceModalProps = DialogVisiblityProps & {
type CollectionMappingChoiceModalProps = DialogVisibilityProps & {
didSelect: (mapping: CollectionMapping) => void;
};

View File

@@ -0,0 +1,253 @@
import type { Collection } from "@/media/collection";
import {
CollectionTileButton,
ItemCard,
ItemTileOverlay,
LargeTileTextOverlay,
} from "@/new/photos/components/Tiles";
import {
canAddToCollection,
canMoveToCollection,
CollectionSummaryOrder,
type CollectionSummaries,
type CollectionSummary,
} from "@/new/photos/services/collection/ui";
import { ensure } from "@/utils/ensure";
import {
Dialog,
DialogContent,
DialogTitle,
styled,
Typography,
useMediaQuery,
} from "@mui/material";
import { t } from "i18next";
import React, { useEffect, useState } from "react";
import { SpaceBetweenFlex } from "./mui";
import {
DialogCloseIconButton,
type DialogVisibilityProps,
} from "./mui/Dialog";
export type CollectionSelectorAction =
| "upload"
| "add"
| "move"
| "restore"
| "unhide";
export interface CollectionSelectorAttributes {
/**
* The {@link action} modifies the title of the dialog, and also removes
* some system collections that don't might not make sense for that
* particular action.
*/
action: CollectionSelectorAction;
/**
* Callback invoked when the user selects one the existing collections
* listed in the dialog.
*/
onSelectCollection: (collection: Collection) => void;
/**
* Callback invoked when the user selects the option to create a new
* collection.
*/
onCreateCollection: () => void;
/**
* Callback invoked when the user cancels the collection selection dialog.
*/
onCancel?: () => void;
/**
* Some actions, like "add" and "move", happen in the context of an existing
* collection. In such cases, their ID can be set as the
* {@link ignoredCollectionID} to omit showing them again in the list of
* collections.
*/
ignoredCollectionID?: number | undefined;
}
type CollectionSelectorProps = DialogVisibilityProps & {
/**
* The same {@link CollectionSelector} can be used for different
* purposes by customizing the {@link attributes} prop before opening it.
*/
attributes: CollectionSelectorAttributes | undefined;
/**
* The collections to list.
*/
collectionSummaries: CollectionSummaries;
/**
* A function to map from a collection ID to a {@link Collection}.
*
* This is invoked when the user makes a selection, to convert the ID of the
* selected collection into a collection object that can be passed to the
* {@link callback} attribute of {@link CollectionSelectorAttributes}.
*/
collectionForCollectionID: (collectionID: number) => Promise<Collection>;
};
/**
* A dialog allowing the user to select one of their existing collections or
* create a new one.
*/
export const CollectionSelector: React.FC<CollectionSelectorProps> = ({
open,
onClose,
attributes,
collectionSummaries,
collectionForCollectionID,
}) => {
// Make the dialog fullscreen if the screen is <= the dialog's max width.
const isFullScreen = useMediaQuery("(max-width: 494px)");
const [filteredCollections, setFilteredCollections] = useState<
CollectionSummary[]
>([]);
useEffect(() => {
if (!attributes || !open) {
return;
}
const collections = [...collectionSummaries.values()]
.filter(({ id, type }) => {
if (id === attributes.ignoredCollectionID) {
return false;
} else if (attributes.action == "add") {
return canAddToCollection(type);
} else if (attributes.action == "upload") {
return canMoveToCollection(type) || type == "uncategorized";
} else if (attributes.action == "restore") {
return canMoveToCollection(type) || type == "uncategorized";
} else {
return canMoveToCollection(type);
}
})
.sort((a, b) => {
return a.name.localeCompare(b.name);
})
.sort((a, b) => {
return (
ensure(CollectionSummaryOrder.get(a.type)) -
ensure(CollectionSummaryOrder.get(b.type))
);
});
if (collections.length === 0) {
onClose();
attributes.onCreateCollection();
}
setFilteredCollections(collections);
}, [collectionSummaries, attributes, open, onClose]);
if (!filteredCollections.length) {
return <></>;
}
if (!attributes) {
return <></>;
}
const { action, onSelectCollection, onCancel, onCreateCollection } =
attributes;
const handleCollectionClick = async (collectionID: number) => {
onSelectCollection(await collectionForCollectionID(collectionID));
onClose();
};
const handleClose = () => {
onCancel?.();
onClose();
};
return (
<Dialog
open={open}
onClose={handleClose}
fullWidth
fullScreen={isFullScreen}
PaperProps={{ sx: { maxWidth: "494px" } }}
>
<SpaceBetweenFlex sx={{ padding: "10px 8px 9px 0" }}>
<DialogTitle variant="h3" fontWeight={"bold"}>
{titleForAction(action)}
</DialogTitle>
<DialogCloseIconButton onClose={handleClose} />
</SpaceBetweenFlex>
<DialogContent_>
<AddCollectionButton onClick={onCreateCollection} />
{filteredCollections.map((collectionSummary) => (
<CollectionButton
key={collectionSummary.id}
collectionSummary={collectionSummary}
onCollectionClick={handleCollectionClick}
/>
))}
</DialogContent_>
</Dialog>
);
};
const DialogContent_ = styled(DialogContent)`
display: flex;
flex-wrap: wrap;
gap: 4px;
`;
const titleForAction = (action: CollectionSelectorAction) => {
switch (action) {
case "upload":
return t("upload_to_album");
case "add":
return t("add_to_album");
case "move":
return t("move_to_album");
case "restore":
return t("restore_to_album");
case "unhide":
return t("unhide_to_album");
}
};
interface CollectionButtonProps {
collectionSummary: CollectionSummary;
onCollectionClick: (collectionID: number) => void;
}
const CollectionButton: React.FC<CollectionButtonProps> = ({
collectionSummary,
onCollectionClick,
}) => (
<ItemCard
TileComponent={CollectionTileButton}
coverFile={collectionSummary.coverFile}
onClick={() => onCollectionClick(collectionSummary.id)}
>
<LargeTileTextOverlay>
<Typography>{collectionSummary.name}</Typography>
</LargeTileTextOverlay>
</ItemCard>
);
interface AddCollectionButtonProps {
onClick: () => void;
}
const AddCollectionButton: React.FC<AddCollectionButtonProps> = ({
onClick,
}) => (
<ItemCard TileComponent={CollectionTileButton} onClick={onClick}>
<LargeTileTextOverlay>{t("create_albums")}</LargeTileTextOverlay>
<PlusOverlay>+</PlusOverlay>
</ItemCard>
);
const PlusOverlay = styled(ItemTileOverlay)`
display: flex;
justify-content: center;
align-items: center;
font-size: 42px;
`;

View File

@@ -1,15 +1,15 @@
import { useIsMobileWidth } from "@/base/hooks";
import { CollectionsSortOptions } from "@/new/photos/components/CollectionsSortOptions";
import {
BarItemTile,
ItemCard,
TileTextOverlay,
} from "@/new/photos/components/ItemCards";
import { FilledIconButton } from "@/new/photos/components/mui";
import {
IMAGE_CONTAINER_MAX_WIDTH,
MIN_COLUMNS,
} from "@/new/photos/components/PhotoList";
import {
BarItemTile,
ItemCard,
TileTextOverlay,
} from "@/new/photos/components/Tiles";
import { UnstyledButton } from "@/new/photos/components/UnstyledButton";
import type {
CollectionSummary,

View File

@@ -5,9 +5,9 @@ import SingleInputForm, {
import { Dialog, DialogContent, DialogTitle } from "@mui/material";
import { t } from "i18next";
import React from "react";
import { type DialogVisiblityProps } from "./mui/Dialog";
import { type DialogVisibilityProps } from "./mui/Dialog";
type NameInputDialogProps = DialogVisiblityProps & {
type NameInputDialogProps = DialogVisibilityProps & {
/** Title of the dialog. */
title: string;
/** Placeholder string to show in the text input when it is empty. */

View File

@@ -1,6 +1,6 @@
import { assertionFailed } from "@/base/assert";
import { useIsMobileWidth } from "@/base/hooks";
import { ItemCard, PreviewItemTile } from "@/new/photos/components/ItemCards";
import { ItemCard, PreviewItemTile } from "@/new/photos/components/Tiles";
import {
isMLSupported,
mlStatusSnapshot,

View File

@@ -7,6 +7,7 @@ import { type EnteFile } from "@/new/photos/types/file";
import { styled } from "@mui/material";
import React, { useEffect, useState } from "react";
import { faceCrop } from "../services/ml";
import { UnstyledButton } from "./UnstyledButton";
interface ItemCardProps {
/**
@@ -135,10 +136,32 @@ export const BarItemTile = styled(ItemTile)`
`;
/**
* A large 150x150 TileComponent used when showing the list of all collections
* in the all collections view.
* A variant of {@link ItemTile} meant for use when the tile is interactable.
*/
export const AllCollectionTile = styled(ItemTile)`
export const ItemTileButton = styled(UnstyledButton)`
/* Buttons reset this to center */
text-align: inherit;
/* Rest of this is mostly verbatim from ItemTile ... */
display: flex;
/* Act as container for the absolutely positioned ItemTileOverlays. */
position: relative;
border-radius: 4px;
overflow: hidden;
& > img {
object-fit: cover;
width: 100%;
height: 100%;
pointer-events: none;
}
`;
/**
* A large 150x150 TileComponent used when showing the list of collections in
* the all collections view and in the collection selector.
*/
export const CollectionTileButton = styled(ItemTileButton)`
width: 150px;
height: 150px;
`;
@@ -171,7 +194,7 @@ export const TileTextOverlay = styled(ItemTileOverlay)`
/**
* A variation of {@link TileTextOverlay} for use with larger tiles like the
* {@link AllCollectionTile}.
* {@link CollectionTile}.
*/
export const LargeTileTextOverlay = styled(ItemTileOverlay)`
padding: 8px;

View File

@@ -6,14 +6,14 @@ import React from "react";
/**
* Common props to control the display of a dialog-like component.
*/
export interface DialogVisiblityProps {
export interface DialogVisibilityProps {
/** If `true`, the dialog is shown. */
open: boolean;
/** Callback fired when the dialog wants to be closed. */
onClose: () => void;
}
type DialogCloseIconButtonProps = Omit<DialogVisiblityProps, "open">;
type DialogCloseIconButtonProps = Omit<DialogVisibilityProps, "open">;
/**
* A convenience {@link IconButton} commonly needed on {@link Dialog}s, at the

View File

@@ -87,24 +87,13 @@ const systemCSTypes = new Set<CollectionSummaryType>([
]);
const addToDisabledCSTypes = new Set<CollectionSummaryType>([
"all",
"archive",
...systemCSTypes,
"incomingShareViewer",
"trash",
"uncategorized",
"defaultHidden",
"hiddenItems",
]);
const moveToDisabledCSTypes = new Set<CollectionSummaryType>([
"all",
"archive",
"incomingShareViewer",
...addToDisabledCSTypes,
"incomingShareCollaborator",
"trash",
"uncategorized",
"defaultHidden",
"hiddenItems",
]);
const hideFromCollectionBarCSTypes = new Set<CollectionSummaryType>([
@@ -114,23 +103,21 @@ const hideFromCollectionBarCSTypes = new Set<CollectionSummaryType>([
"defaultHidden",
]);
export const hasNonSystemCollections = (
collectionSummaries: CollectionSummaries,
) => {
for (const collectionSummary of collectionSummaries.values()) {
if (!isSystemCollection(collectionSummary.type)) return true;
}
return false;
};
export const isSystemCollection = (type: CollectionSummaryType) =>
systemCSTypes.has(type);
export const canMoveToCollection = (type: CollectionSummaryType) =>
!moveToDisabledCSTypes.has(type);
export const areOnlySystemCollections = (
collectionSummaries: CollectionSummaries,
) =>
[...collectionSummaries.values()].every(({ type }) =>
isSystemCollection(type),
);
export const canAddToCollection = (type: CollectionSummaryType) =>
!addToDisabledCSTypes.has(type);
export const isSystemCollection = (type: CollectionSummaryType) =>
systemCSTypes.has(type);
export const canMoveToCollection = (type: CollectionSummaryType) =>
!moveToDisabledCSTypes.has(type);
export const shouldShowOnCollectionBar = (type: CollectionSummaryType) =>
!hideFromCollectionBarCSTypes.has(type);