[web] Use lighter scrim for overlays atop photo viewer (#4978)

\+ continue with the custom title bar prep
This commit is contained in:
Manav Rathi
2025-02-06 09:23:12 +05:30
committed by GitHub
10 changed files with 215 additions and 62 deletions

View File

@@ -4,7 +4,7 @@ import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton";
import log from "@/base/log";
import type { Collection } from "@/media/collection";
import { useSettingsSnapshot } from "@/new/photos/components/utils/use-snapshot";
import { photosDialogZ } from "@/new/photos/components/utils/z-index";
import { aboveGalleryContentZ } from "@/new/photos/components/utils/z-index";
import {
publishCastPayload,
revokeAllCastTokens,
@@ -134,7 +134,7 @@ export const AlbumCastDialog: React.FC<AlbumCastDialogProps> = ({
open={open}
onClose={onClose}
title={t("cast_album_to_tv")}
sx={{ zIndex: photosDialogZ }}
sx={{ zIndex: aboveGalleryContentZ }}
>
{view == "choose" && (
<Stack sx={{ py: 1, gap: 4 }}>

View File

@@ -1,5 +1,5 @@
import { Notification } from "@/new/photos/components/Notification";
import { photosDialogZ } from "@/new/photos/components/utils/z-index";
import { aboveGalleryContentZ } from "@/new/photos/components/utils/z-index";
import { useAppContext } from "@/new/photos/types/context";
import { t } from "i18next";
import { GalleryContext } from "pages/gallery";
@@ -120,7 +120,7 @@ export const FilesDownloadProgress: React.FC<FilesDownloadProgressProps> = ({
horizontal="left"
sx={{
"&&": { bottom: `${index * 80 + 20}px` },
zIndex: photosDialogZ,
zIndex: aboveGalleryContentZ,
}}
open={isFilesDownloadStarted(attributes)}
onClose={handleClose(attributes)}

View File

@@ -29,8 +29,8 @@ import {
} from "@/new/photos/components/utils/dialog";
import { useSettingsSnapshot } from "@/new/photos/components/utils/use-snapshot";
import {
aboveGalleryContentZ,
fileInfoDrawerZ,
photosDialogZ,
} from "@/new/photos/components/utils/z-index";
import { tagNumericValue, type RawExifTags } from "@/new/photos/services/exif";
import {
@@ -391,9 +391,24 @@ const FileInfoSidebar = styled(
(props: Pick<DialogProps, "open" | "onClose" | "children">) => (
<SidebarDrawer {...props} anchor="right" />
),
)({
)(({ theme }) => ({
zIndex: fileInfoDrawerZ,
});
// [Note: Lighter backdrop for overlays on photo viewer]
//
// The default backdrop color we use for the drawer in light mode is too
// "white" when used in the image gallery because unlike the rest of the app
// the gallery retains a black background irrespective of the mode. So use a
// lighter scrim when overlaying content directly atop the image gallery.
//
// We don't need to add this special casing for nested overlays (e.g.
// dialogs initiated from the file info drawer itself) since now there is
// enough "white" on the screen to warrant the stronger (default) backdrop.
...theme.applyStyles("light", {
".MuiBackdrop-root": {
backgroundColor: theme.vars.palette.backdrop.faint,
},
}),
}));
interface InfoItemProps {
/**
@@ -801,7 +816,7 @@ const FileNameEditDialog = ({
};
return (
<TitledMiniDialog
sx={{ zIndex: photosDialogZ }}
sx={{ zIndex: aboveGalleryContentZ }}
open={isInEditMode}
onClose={closeEditMode}
title={t("rename_file")}

View File

@@ -12,7 +12,7 @@ import log from "@/base/log";
import { downloadAndRevokeObjectURL } from "@/base/utils/web";
import { downloadManager } from "@/gallery/services/download";
import { EnteFile } from "@/media/file";
import { photosDialogZ } from "@/new/photos/components/utils/z-index";
import { aboveGalleryContentZ } from "@/new/photos/components/utils/z-index";
import { getLocalCollections } from "@/new/photos/services/collections";
import { AppContext } from "@/new/photos/types/context";
import { CenteredFlex } from "@ente/shared/components/Container";
@@ -525,7 +525,7 @@ export const ImageEditorOverlay: React.FC<ImageEditorOverlayProps> = (
<Backdrop
sx={{
backgroundColor: "background.default" /* Opaque */,
zIndex: photosDialogZ,
zIndex: aboveGalleryContentZ,
width: "100%",
}}
open

View File

@@ -3,13 +3,17 @@ import { assertionFailed } from "@/base/assert";
import { Overlay } from "@/base/components/containers";
import { FilledIconButton, type ButtonishProps } from "@/base/components/mui";
import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator";
import { type ModalVisibilityProps } from "@/base/components/utils/modal";
import {
useModalVisibility,
type ModalVisibilityProps,
} from "@/base/components/utils/modal";
import { lowercaseExtension } from "@/base/file-name";
import log from "@/base/log";
import { downloadManager } from "@/gallery/services/download";
import { fileLogID, type EnteFile } from "@/media/file";
import { FileType } from "@/media/file-type";
import { isHEICExtension, needsJPEGConversion } from "@/media/formats";
import { ConfirmDeleteFileDialog } from "@/new/photos/components/PhotoViewer";
import { moveToTrash } from "@/new/photos/services/collection";
import { extractRawExif, parseExif } from "@/new/photos/services/exif";
import { AppContext } from "@/new/photos/types/context";
@@ -134,8 +138,7 @@ export const PhotoViewer: React.FC<PhotoViewerProps> = ({
onSelectPerson,
}) => {
const galleryContext = useContext(GalleryContext);
const { showLoadingBar, hideLoadingBar, showMiniDialog } =
useContext(AppContext);
const { showLoadingBar, hideLoadingBar } = useContext(AppContext);
const publicCollectionGalleryContext = useContext(
PublicCollectionGalleryContext,
);
@@ -183,6 +186,12 @@ export const PhotoViewer: React.FC<PhotoViewerProps> = ({
() => downloadManager.fileDownloadProgressSnapshot(),
);
const {
show: showConfirmDeleteFile,
props: confirmDeleteFileVisibilityProps,
} = useModalVisibility();
const [fileToDelete, setFileToDelete] = useState<EnteFile | undefined>();
useEffect(() => {
if (!pswpElement.current) return;
if (isOpen) {
@@ -227,7 +236,7 @@ export const PhotoViewer: React.FC<PhotoViewerProps> = ({
break;
case "Backspace":
case "Delete":
confirmTrashFile(photoSwipe?.currItem as EnteFile);
confirmDeleteFile(photoSwipe?.currItem as EnteFile);
break;
case "d":
case "D":
@@ -512,36 +521,21 @@ export const PhotoViewer: React.FC<PhotoViewerProps> = ({
needUpdate.current = true;
};
const trashFile = async (file: DisplayFile) => {
try {
showLoadingBar();
try {
await moveToTrash([file]);
} finally {
hideLoadingBar();
}
markTempDeleted?.([file]);
updateItems(items.filter((item) => item.id !== file.id));
needUpdate.current = true;
} catch (e) {
log.error("trashFile failed", e);
}
};
const confirmTrashFile = (file: EnteFile) => {
const confirmDeleteFile = (file: EnteFile) => {
if (!file || !isOwnFile || isTrashCollection) {
return;
}
showMiniDialog({
title: t("trash_file_title"),
message: t("trash_file_message"),
continue: {
text: t("move_to_trash"),
color: "critical",
action: () => trashFile(file),
autoFocus: true,
},
});
setFileToDelete(file);
showConfirmDeleteFile();
};
const handleDeleteFile = async () => {
const file = fileToDelete!;
await moveToTrash([file]);
markTempDeleted?.([file]);
updateItems(items.filter((item) => item.id !== file.id));
setFileToDelete(undefined);
needUpdate.current = true;
};
const handleArrowClick = (
@@ -842,7 +836,7 @@ export const PhotoViewer: React.FC<PhotoViewerProps> = ({
className="pswp__button pswp__button--custom"
title={t("delete_key")}
onClick={() => {
confirmTrashFile(
confirmDeleteFile(
photoSwipe?.currItem as EnteFile,
);
}}
@@ -942,6 +936,10 @@ export const PhotoViewer: React.FC<PhotoViewerProps> = ({
</div>
</div>
</div>
<ConfirmDeleteFileDialog
{...confirmDeleteFileVisibilityProps}
onConfirm={handleDeleteFile}
/>
<FileInfo
showInfo={showInfo}
handleCloseInfo={handleCloseInfo}

View File

@@ -31,7 +31,7 @@ import {
updateReadyToInstallDialogAttributes,
} from "@/new/photos/components/utils/download";
import { useLoadingBar } from "@/new/photos/components/utils/use-loading-bar";
import { photosDialogZ } from "@/new/photos/components/utils/z-index";
import { aboveGalleryContentZ } from "@/new/photos/components/utils/z-index";
import { runMigrations } from "@/new/photos/services/migration";
import { initML, isMLSupported } from "@/new/photos/services/ml";
import { getFamilyPortalRedirectURL } from "@/new/photos/services/user-details";
@@ -186,7 +186,7 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
<ThemedLoadingBar ref={loadingBarRef} />
<AttributedMiniDialog
sx={{ zIndex: photosDialogZ }}
sx={{ zIndex: aboveGalleryContentZ }}
{...miniDialogProps}
/>
@@ -227,9 +227,6 @@ const WindowTitlebar: React.FC<React.PropsWithChildren> = ({ children }) => (
// See: [Note: Customize the desktop title bar]
const WindowTitlebarArea = styled(CenteredFlex)`
z-index: 10000;
background-color: var(--mui-palette-backdrop-muted);
backdrop-filter: blur(3px);
width: 100%;
height: env(titlebar-area-height, 30px /* fallback */);
display: flex;

View File

@@ -338,8 +338,7 @@ export const AttributedMiniDialog: React.FC<
);
};
type TitledMiniDialogProps = Pick<DialogProps, "open" | "sx"> & {
onClose: () => void;
type TitledMiniDialogProps = Pick<DialogProps, "open" | "onClose" | "sx"> & {
/**
* The dialog's title.
*/

View File

@@ -19,19 +19,46 @@ import type { ModalVisibilityProps } from "../utils/modal";
*
* It is width limited to 375px, and always at full width. It also has a default
* padding.
*
* It also does some trickery with a sticky opaque bar to ensure that the
* content scrolls below our inline title bar on desktop.
*/
export const SidebarDrawer = styled(Drawer)(({ theme }) => ({
"& .MuiPaper-root": {
maxWidth: "375px",
width: "100%",
scrollbarWidth: "thin",
padding: theme.spacing(1),
// Add extra padding on the top to account for our inline title bar.
// See: [Note: Customize the desktop title bar]
...(wipDesktopCustomTitlebar
? { paddingTop: "calc(env(titlebar-area-height) / 2 + 4px)" }
: {}),
},
export const SidebarDrawer: React.FC<DrawerProps> = ({ children, ...rest }) => (
<Drawer
{...rest}
PaperProps={{
sx: {
maxWidth: "375px",
width: "100%",
scrollbarWidth: "thin",
// Need to increase specificity to override inherited padding.
"&&": { padding: 0 },
},
}}
>
{wipDesktopCustomTitlebar && <AppTitlebarBackdrop />}
<Box sx={{ p: 1 }}>{children}</Box>
</Drawer>
);
/**
* When running on desktop, we adds a sticky opaque bar at the top of the
* sidebar with a z-index greater than the expected sidebar contents. This
* ensures that any title bar overlays added by the system (e.g. the traffic
* lights on macOS) have a opaque-ish background and the sidebar contents scroll
* underneath them.
*
* See: [Note: Customize the desktop title bar]
*/
const AppTitlebarBackdrop = styled("div")(({ theme }) => ({
position: "sticky",
top: 0,
left: 0,
width: "100%",
minHeight: "env(titlebar-area-height, 30px)",
bgcolor: theme.vars.palette.backdrop.muted,
backdropFilter: "blur(3px)",
zIndex: 10000,
}));
/**

View File

@@ -0,0 +1,95 @@
import { InlineErrorIndicator } from "@/base/components/ErrorIndicator";
import { TitledMiniDialog } from "@/base/components/MiniDialog";
import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton";
import { LoadingButton } from "@/base/components/mui/LoadingButton";
import type { ModalVisibilityProps } from "@/base/components/utils/modal";
import log from "@/base/log";
import { Stack, Typography, type ModalProps } from "@mui/material";
import { t } from "i18next";
import { useState } from "react";
import { aboveGalleryContentZ } from "./utils/z-index";
type ConfirmDeleteFileDialogProps = ModalVisibilityProps & {
/**
* Called when the user confirms the deletion.
*
* The delete button will show an activity indicator until this async
* operation completes.
*/
onConfirm: () => Promise<void>;
};
/**
* A bespoke variant of AttributedMiniDialog for use by the delete file
* confirmation prompt that we show in the image viewer.
*
* - It auto focuses the primary action.
* - It uses a lighter backdrop in light mode.
*/
export const ConfirmDeleteFileDialog: React.FC<
ConfirmDeleteFileDialogProps
> = ({ open, onClose, onConfirm }) => {
const [phase, setPhase] = useState<"loading" | "failed" | undefined>();
const resetPhaseAndClose = () => {
setPhase(undefined);
onClose();
};
const handleClick = async () => {
setPhase("loading");
try {
await onConfirm();
resetPhaseAndClose();
} catch (e) {
log.error(e);
setPhase("failed");
}
};
const handleClose: ModalProps["onClose"] = (_, reason) => {
// Ignore backdrop clicks when we're processing the user request.
if (reason == "backdropClick" && phase == "loading") return;
resetPhaseAndClose();
};
return (
<TitledMiniDialog
open={open}
onClose={handleClose}
title={t("trash_file_title")}
sx={(theme) => ({
zIndex: aboveGalleryContentZ,
// See: [Note: Lighter backdrop for overlays on photo viewer]
...theme.applyStyles("light", {
".MuiBackdrop-root": {
backgroundColor: theme.vars.palette.backdrop.faint,
},
}),
})}
>
<Typography sx={{ color: "text.muted" }}>
{t("trash_file_message")}
</Typography>
<Stack sx={{ paddingBlockStart: "24px", gap: "8px" }}>
{phase == "failed" && <InlineErrorIndicator />}
<LoadingButton
loading={phase == "loading"}
fullWidth
color="critical"
autoFocus
onClick={handleClick}
>
{t("move_to_trash")}
</LoadingButton>
<FocusVisibleButton
fullWidth
color="secondary"
disabled={phase == "loading"}
onClick={resetPhaseAndClose}
>
{t("cancel")}
</FocusVisibleButton>
</Stack>
</TitledMiniDialog>
);
};

View File

@@ -1,3 +1,25 @@
/**
* @file [Note: Custom z-indices]
*
* The default MUI z-index values (as of 6.4) are
* https://mui.com/material-ui/customization/default-theme/
*
* zIndex: Object
* - mobileStepper: 1000
* - fab: 1050
* - speedDial: 1050
* - appBar: 1100
* - drawer: 1200
* - modal: 1300
* - snackbar: 1400
* - tooltip: 1500
*
* We don't customize any of those, but photoswipe, the library we use for the
* image gallery, sets its base zIndex to a high value, so we need to tweak the
* zIndices of components that need to appear atop it accordingly. This file
* tries to hold those customizations.
*/
/**
* PhotoSwipe sets the zIndex of its "pswp" class to 1500. We need to go higher
* than that for our drawers and dialogs to get them to show above it.
@@ -14,4 +36,4 @@ export const fileInfoDrawerZ = photoSwipeZ + 1;
* are visible above the drawer in case they are shown in response to some
* action taken in the file info drawer.
*/
export const photosDialogZ = 1600;
export const aboveGalleryContentZ = fileInfoDrawerZ + 1;