[web] Use lighter scrim for overlays atop photo viewer (#4978)
\+ continue with the custom title bar prep
This commit is contained in:
@@ -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 }}>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
|
||||
/**
|
||||
|
||||
95
web/packages/new/photos/components/PhotoViewer.tsx
Normal file
95
web/packages/new/photos/components/PhotoViewer.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user