Skip to content

Commit

Permalink
[web] Use upstream PhotoSwipe (WIP) (#5097)
Browse files Browse the repository at this point in the history
Continue #5066
  • Loading branch information
mnvr authored Feb 17, 2025
2 parents d36934e + 00db3c0 commit 717dc09
Show file tree
Hide file tree
Showing 12 changed files with 331 additions and 196 deletions.
2 changes: 1 addition & 1 deletion web/apps/photos/src/components/PhotoFrame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from "@/gallery/services/download";
import { EnteFile } from "@/media/file";
import { FileType } from "@/media/file-type";
import { FileViewer } from "@/new/photos/components/FileViewer";
import { FileViewer } from "@/new/photos/components/FileViewerComponents";
import type { GalleryBarMode } from "@/new/photos/components/gallery/reducer";
import { TRASH_SECTION } from "@/new/photos/services/collection";
import { styled } from "@mui/material";
Expand Down
6 changes: 3 additions & 3 deletions web/apps/photos/src/components/PhotoViewer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type { Collection } from "@/media/collection";
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/FileViewer";
import { ConfirmDeleteFileDialog } from "@/new/photos/components/FileViewerComponents";
import { ImageEditorOverlay } from "@/new/photos/components/ImageEditorOverlay";
import { moveToTrash } from "@/new/photos/services/collection";
import { extractRawExif, parseExif } from "@/new/photos/services/exif";
Expand Down Expand Up @@ -992,8 +992,8 @@ export const PhotoViewer: React.FC<PhotoViewerProps> = ({
onConfirm={handleDeleteFile}
/>
<FileInfo
showInfo={showInfo}
handleCloseInfo={handleCloseInfo}
open={showInfo}
onClose={handleCloseInfo}
file={photoSwipe?.currItem as EnteFile}
exif={exif?.value}
shouldDisableEdits={!isOwnFile}
Expand Down
2 changes: 1 addition & 1 deletion web/apps/photos/src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import { photosLogout } from "services/logout";

import "photoswipe/dist/photoswipe.css";
// TODO(PS): Note, auto hide only works with the new CSS.
// import "../../../../packages/new/photos/components/ps5/dist/photoswipe.css";
// import "../../../../packages/gallery/components/viewer/ps5/dist/photoswipe.css";

import "styles/global.css";

Expand Down
6 changes: 6 additions & 0 deletions web/apps/photos/src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ body {
display: none;
}

.pswp-ente {
/* The default z-index for PhotoSwipe is 10k, way beyond everything else.
Give it a more moderate value so that MUI elements can be used with it. */
z-index: calc(var(--mui-zIndex-drawer) - 1);
}

/*
Make the controllable video elements we render as custom PhotoSwipe content
take up the entire container.
Expand Down
2 changes: 2 additions & 0 deletions web/packages/base/components/utils/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,8 @@ const components: Components = {

MuiDialog: {
defaultProps: {
// [Note: Overzealous Chrome? Complicated ARIA?]
//
// This is required to prevent console errors about aria-hiding a
// focused button when the dialog is closed.
//
Expand Down
32 changes: 21 additions & 11 deletions web/packages/gallery/components/FileInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator";
import { SidebarDrawer } from "@/base/components/mui/SidebarDrawer";
import { Titlebar } from "@/base/components/Titlebar";
import { EllipsizedTypography } from "@/base/components/Typography";
import { useModalVisibility } from "@/base/components/utils/modal";
import {
useModalVisibility,
type ModalVisibilityProps,
} from "@/base/components/utils/modal";
import { useBaseContext } from "@/base/context";
import { haveWindow } from "@/base/env";
import { nameAndExtension } from "@/base/file-name";
Expand Down Expand Up @@ -107,13 +110,11 @@ export interface FileInfoExif {
parsed: ParsedMetadata | undefined;
}

export interface FileInfoProps {
export type FileInfoProps = ModalVisibilityProps & {
/**
* The file whose information we are showing.
*/
file: EnteFile | undefined;
showInfo: boolean;
handleCloseInfo: () => void;
exif: FileInfoExif | undefined;
/**
* TODO: Rename and flip to allowEdits.
Expand All @@ -138,14 +139,14 @@ export interface FileInfoProps {
* Called when the user selects a person in the file info panel.
*/
onSelectPerson?: ((personID: string) => void) | undefined;
}
};

export const FileInfo: React.FC<FileInfoProps> = ({
open,
onClose,
file,
shouldDisableEdits,
allowMap,
showInfo,
handleCloseInfo,
exif,
scheduleUpdate,
refreshPhotoswipe,
Expand Down Expand Up @@ -209,8 +210,8 @@ export const FileInfo: React.FC<FileInfoProps> = ({
onSelectPerson?.(personID);

return (
<FileInfoSidebar open={showInfo} onClose={handleCloseInfo}>
<Titlebar onClose={handleCloseInfo} title={t("info")} backIsClose />
<FileInfoSidebar open={open} onClose={onClose}>
<Titlebar onClose={onClose} title={t("info")} backIsClose />
<Stack sx={{ pt: 1, pb: 3, gap: "20px" }}>
<RenderCaption
{...{
Expand Down Expand Up @@ -343,7 +344,7 @@ export const FileInfo: React.FC<FileInfoProps> = ({
</Stack>
<RawExif
{...rawExifVisibilityProps}
onInfoClose={handleCloseInfo}
onInfoClose={onClose}
tags={exif?.tags}
fileName={file.metadata.title}
/>
Expand Down Expand Up @@ -400,7 +401,16 @@ const parseExifInfo = (

const FileInfoSidebar = styled(
(props: Pick<DialogProps, "open" | "onClose" | "children">) => (
<SidebarDrawer {...props} anchor="right" />
<SidebarDrawer
{...props}
anchor="right"
// See: [Note: Overzealous Chrome? Complicated ARIA?], but this time
// with a different workaround.
//
// https://github.com/mui/material-ui/issues/43106#issuecomment-2514637251
disableRestoreFocus={true}
closeAfterTransition={true}
/>
),
)(({ theme }) => ({
zIndex: fileInfoDrawerZ,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) {
throw new Error("Whoa");
}

import { useModalVisibility } from "@/base/components/utils/modal";
import { FileInfo } from "@/gallery/components/FileInfo";
import type { EnteFile } from "@/media/file.js";
import { Button, styled } from "@mui/material";
import { useCallback, useEffect, useRef } from "react";
import { FileViewerPhotoSwipe } from "./FileViewerPhotoSwipe";
import { useCallback, useEffect, useRef, useState } from "react";
import { FileViewerPhotoSwipe } from "./photoswipe";

export interface FileViewerProps {
/**
Expand Down Expand Up @@ -57,8 +59,23 @@ const FileViewer: React.FC<FileViewerProps> = ({
}) => {
const pswpRef = useRef<FileViewerPhotoSwipe | undefined>();

// Whenever we get a callback from our custom PhotoSwipe instance, we also
// get the active file on which that action was performed as an argument.
// Save it as a prop so that the rest of our React tree can use it.
//
// This is not guaranteed, or even intended, to be in sync with the active
// file shown within the file viewer. All that this guarantees is this will
// refer to the file on which the last user initiated action was performed.
const [activeFile, setActiveFile] = useState<EnteFile | undefined>(
undefined,
);

const { show: showFileInfo, props: fileInfoVisibilityProps } =
useModalVisibility();

const handleViewInfo = useCallback((file: EnteFile) => {
console.log("view-info", file);
setActiveFile(file);
showFileInfo();
}, []);

useEffect(() => {
Expand All @@ -85,6 +102,7 @@ const FileViewer: React.FC<FileViewerProps> = ({
return (
<Container>
<Button>Test</Button>
<FileInfo {...fileInfoVisibilityProps} file={activeFile} />
</Container>
);
};
Expand Down
181 changes: 181 additions & 0 deletions web/packages/gallery/components/viewer/data-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/* xeslint-disable */
// x-@ts-nocheck

import {
downloadManager,
type LivePhotoSourceURL,
} from "@/gallery/services/download";
import type { EnteFile } from "@/media/file";
import { FileType } from "@/media/file-type";

// TODO(PS):
//import { type SlideData } from "./ps5/dist/types/slide/"
interface SlideData {
/**
* image URL
*/
src?: string | undefined;
/**
* image width
*/
width?: number | undefined;
/**
* image height
*/
height?: number | undefined;
/**
* html content of a slide
*/
html?: string | undefined;
}

type ItemData = SlideData & {
// Our props. TODO(PS) document if end up using these.
videoURL?: string;
livePhotoVideoURL?: string;
isContentLoading?: boolean;
isContentZoomable?: boolean;
};

/**
* A class that stores and serves data required by our custom PhotoSwipe
* instance, effectively acting as an in-memory cache.
*
* By keeping this independent of the lifetime of the PhotoSwipe instance, we
* can reuse the same cache for multiple displays of our file viewer.
*/
export class FileViewerDataSource {
private itemDataByFileID = new Map<number, ItemData>();
private needsRefreshByFileID = new Map<number, () => void>();

/**
* Return the best available ItemData for rendering the given {@link file}.
*
* If an entry does not exist for a particular file, then it is lazily added
* on demand, and updated as we keep getting better data (thumbnail,
* original) for the file.
*
* At each step, we call the provided callback so that file viewer can call
* us again to get the updated data.
*
* ---
*
* Detailed flow:
*
* If we already have the final data about the file, then this function will
* return it and do nothing subsequently.
*
* Otherwise, it will:
*
* 1. Return empty slide data; PhotoSwipe will not show anything in the
* image area but will otherwise render UI controls properly (in most
* cases a cached renderable thumbnail URL will be available shortly)
*
* 2. Insert empty data so that we don't enqueue multiple updates, and
* return this empty data.
*
* Then it we start fetching data for the file.
*
* First it'll fetch the thumbnail. Once that is done, it'll update the data
* it has cached, and notify the caller (using the provided callback) so it
* can refresh the slide.
*
* Then it'll continue fetching the original.
*
* - For images and videos, this will be the single original.
*
* - For live photos, this will also be a two step process, first with the
* original image, then again with the video component.
*
* At this point, the data for this file will be considered final, and
* subsequent calls for the same file will return this same value unless it
* is invalidated.
*
* If at any point an error occurs, we reset our cache so that the next time
* the data is requested we repeat the process instead of continuing to
* serve the incomplete result.
*/
itemDataForFile(file: EnteFile, needsRefresh: () => void) {
let itemData = this.itemDataByFileID.get(file.id);
// We assume that there is only one file viewer that is using us
// at a given point of time. This assumption is currently valid.
this.needsRefreshByFileID.set(file.id, needsRefresh);

if (!itemData) {
itemData = {};
this.itemDataByFileID.set(file.id, itemData);
void this.enqueueUpdates(file);
}

return itemData;
}

private async enqueueUpdates(file: EnteFile) {
const update = (itemData: ItemData) => {
this.itemDataByFileID.set(file.id, itemData);
this.needsRefreshByFileID.get(file.id)?.();
};

const thumbnailURL = await downloadManager.renderableThumbnailURL(file);
// TODO(PS):
const thumbnailData = await withDimensions(thumbnailURL!);
update({
...thumbnailData,
isContentLoading: true,
isContentZoomable: false,
});

switch (file.metadata.fileType) {
case FileType.image: {
const sourceURLs =
await downloadManager.renderableSourceURLs(file);
// TODO(PS):
const itemData = await withDimensions(sourceURLs.url as string);
update(itemData);
break;
}

case FileType.video: {
const sourceURLs =
await downloadManager.renderableSourceURLs(file);
// TODO(PS):
update({ videoURL: sourceURLs.url as string });
break;
}

case FileType.livePhoto: {
const sourceURLs =
await downloadManager.renderableSourceURLs(file);
const livePhotoSourceURLs =
sourceURLs.url as LivePhotoSourceURL;
const imageURL = await livePhotoSourceURLs.image();
// TODO(PS):
const imageData = await withDimensions(imageURL!);
update(imageData);
const livePhotoVideoURL = await livePhotoSourceURLs.video();
update({ ...imageData, livePhotoVideoURL });
break;
}
}
}
}

/**
* Take a image URL, determine its dimensions using browser APIs, and return the URL
* and its dimensions in a form that can directly be passed to PhotoSwipe as
* {@link ItemData}.
*/
const withDimensions = (imageURL: string): Promise<ItemData> =>
new Promise((resolve) => {
const image = new Image();
image.onload = () => {
resolve({
src: imageURL,
width: image.naturalWidth,
height: image.naturalHeight,
});
};
// image.onerror = ()
// TODO(PS): Handle imageElement.onerror
image.src = imageURL;
});
Loading

0 comments on commit 717dc09

Please sign in to comment.