Skip to content

Commit

Permalink
Integrate
Browse files Browse the repository at this point in the history
  • Loading branch information
mnvr committed Feb 17, 2025
1 parent 12a96b6 commit 9d76d93
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 154 deletions.
138 changes: 77 additions & 61 deletions web/packages/gallery/components/viewer/data-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { FileType } from "@/media/file-type";

// TODO(PS):
//import { type SlideData } from "./ps5/dist/types/slide/"
export interface SlideData {
interface SlideData {
/**
* image URL
*/
Expand All @@ -27,13 +27,16 @@ export interface SlideData {
* 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;
isFinal?: boolean;
}
};

/**
* A class that stores and serves data required by our custom PhotoSwipe
Expand All @@ -43,74 +46,83 @@ export interface SlideData {
* 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>();

/**
* The best available SlideData for rendering the file with the given ID.
* Return the best available ItemData for rendering the given {@link file}.
*
* If an entry does not exist for a particular fileID, then it is lazily
* added on demand, and updated as we keep getting better data (thumbnail,
* 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.
*/
private itemDataByFileID = new Map<number, SlideData>();

/**
*
* The {@link onUpdate} callback is invoked each time we have data about the
* given {@link 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:
*
* If we already have the final data about file, then {@link onUpdate} will
* be called once with this final {@link itemData}. Otherwise it'll be
* called multiple times.
* 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)
*
* 1. First with empty itemData.
* 2. Insert empty data so that we don't enqueue multiple updates, and
* return this empty data.
*
* 2. Then with the thumbnail data.
* Then it we start fetching data for the file.
*
* 3. Then with the original. For live photos, this will happen twice, first
* with the original image, then again with the video component.
* 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.
*
* 4. 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.
* Then it'll continue fetching the original.
*
* The same entry might get updated multiple times, as we start with the
* thumbnail but then also update this as we keep getting more of the
* original (e.g. for a live photo, it'll be updated once when we get the
* original image, and then again later once we get the original video).
* - For images and videos, this will be the single original.
*
* @param index
* @param file
* @param onUpdate Callback invoked each time we have data about the given
* {@link file}.
* - 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.
*/
private async enqueueUpdates(
file: EnteFile,
onUpdate: (itemData: SlideData) => void,
) {
const update = (itemData: SlideData) => {
itemDataForFile(file: EnteFile, needsRefresh: () => void) {
let itemData = this.itemDataByFileID.get(file.id);
if (itemData) {
if (!itemData.isFinal) {
// 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);
}
} else {
itemData = {};
this.itemDataByFileID.set(file.id, itemData);
onUpdate(itemData);
};
this.needsRefreshByFileID.set(file.id, needsRefresh);
void this.enqueueUpdates(file);
}

// We might not have anything to show immediately, though in most cases
// a cached renderable thumbnail URL will be available shortly.
//
// Meanwhile,
//
// 1. Return empty slide data; PhotoSwipe will not show anything in the
// image area but will otherwise render UI controls properly.
//
// 2. Insert empty data so that we don't enqueue multiple updates.
return itemData;
}

const itemData = this.itemDataByFileID.get(file.id);
if (itemData) {
itemData = {};
this.itemDataByFileID.set(file.id, itemData);
this.enqueueUpdates(index, file);
}
}
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);
const thumbnailData = await augmentedWithDimensions(thumbnailURL);
// TODO(PS):
const thumbnailData = await withDimensions(thumbnailURL!);
update({
...thumbnailData,
isContentLoading: true,
Expand All @@ -121,25 +133,29 @@ export class FileViewerDataSource {
case FileType.image: {
const sourceURLs =
await downloadManager.renderableSourceURLs(file);
update(await augmentedWithDimensions(sourceURLs.url));
// TODO(PS):
update(await withDimensions(sourceURLs.url as string));
break;
}

case FileType.video: {
const sourceURLs =
await downloadManager.renderableSourceURLs(file);
const disableDownload = !!this.opts.disableDownload;
update({ html: videoHTML(sourceURLs.url, disableDownload) });
// const disableDownload = !!this.opts.disableDownload;
// update({ html: videoHTML(sourceURLs.url, disableDownload) });
// TODO(PS):
update({ videoURL: sourceURLs.url as string });
break;
}

default: {
case FileType.livePhoto: {
const sourceURLs =
await downloadManager.renderableSourceURLs(file);
const livePhotoSourceURLs =
sourceURLs.url as LivePhotoSourceURL;
const imageURL = await livePhotoSourceURLs.image();
const imageData = await augmentedWithDimensions(imageURL);
// TODO(PS):
const imageData = await withDimensions(imageURL!);
update(imageData);
const livePhotoVideoURL = await livePhotoSourceURLs.video();
update({ ...imageData, livePhotoVideoURL });
Expand All @@ -152,9 +168,9 @@ export class FileViewerDataSource {
/**
* 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 SlideData}.
* {@link ItemData}.
*/
const augmentedWithDimensions = (imageURL: string): Promise<SlideData> =>
const withDimensions = (imageURL: string): Promise<ItemData> =>
new Promise((resolve) => {
const image = new Image();
image.onload = () => {
Expand Down
107 changes: 14 additions & 93 deletions web/packages/gallery/components/viewer/photoswipe.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
/* eslint-disable */
// @ts-nocheck

import { assertionFailed } from "@/base/assert";
import log from "@/base/log";
import {
downloadManager,
type LivePhotoSourceURL,
} from "@/gallery/services/download";
import type { EnteFile } from "@/media/file";
import { FileType } from "@/media/file-type";
import { t } from "i18next";
import { FileViewerDataSource, type SlideData } from "./data-source";
import { FileViewerDataSource } from "./data-source";
import type { FileViewerProps } from "./FileViewer";
import { createPSRegisterElementIconHTML } from "./icons";

Expand Down Expand Up @@ -167,36 +161,27 @@ export class FileViewerPhotoSwipe {
});

pswp.addFilter("itemData", (_, index) => {
const file = files[index];
const file = files[index]!;

// We might not have anything to show immediately, though in most
// cases a cached renderable thumbnail URL will be available
// shortly.
//
// Meanwhile,
//
// 1. Return empty slide data; PhotoSwipe will not show anything in
// the image area but will otherwise render UI controls properly.
//
// 2. Insert empty data so that we don't enqueue multiple updates.

let itemData: SlideData | undefined;
if (file) {
itemData = this.itemDataByFileID.get(file.id);
if (!itemData) {
itemData = {};
this.itemDataByFileID.set(file.id, itemData);
this.enqueueUpdates(index, file);
}
let itemData = this.dataSource.itemDataForFile(file, () => {
this.pswp.refreshSlideContent(index);
});

const { videoURL, ...rest } = itemData;
if (videoURL) {
const disableDownload = !!this.opts.disableDownload;
itemData = {
...rest,
html: videoHTML(videoURL, disableDownload),
};
}

log.debug(() => ["[viewer]", { index, itemData, file }]);
if (!file) assertionFailed();

if (this.lastActivityDate != "already-hidden")
this.lastActivityDate = new Date();

return itemData ?? {};
return itemData;
});

pswp.addFilter("isContentLoading", (isLoading, content) => {
Expand Down Expand Up @@ -390,72 +375,8 @@ export class FileViewerPhotoSwipe {
// TODO(PS): Commented during testing
// this.pswp.element.classList.remove("pswp--ui-visible");
}

private async enqueueUpdates(index: number, file: EnteFile) {
const update = (itemData: SlideData) => {
this.itemDataByFileID.set(file.id, itemData);
this.pswp.refreshSlideContent(index);
};

const thumbnailURL = await downloadManager.renderableThumbnailURL(file);
const thumbnailData = await augmentedWithDimensions(thumbnailURL);
update({
...thumbnailData,
isContentLoading: true,
isContentZoomable: false,
});

switch (file.metadata.fileType) {
case FileType.image: {
const sourceURLs =
await downloadManager.renderableSourceURLs(file);
update(await augmentedWithDimensions(sourceURLs.url));
break;
}

case FileType.video: {
const sourceURLs =
await downloadManager.renderableSourceURLs(file);
const disableDownload = !!this.opts.disableDownload;
update({ html: videoHTML(sourceURLs.url, disableDownload) });
break;
}

default: {
const sourceURLs =
await downloadManager.renderableSourceURLs(file);
const livePhotoSourceURLs =
sourceURLs.url as LivePhotoSourceURL;
const imageURL = await livePhotoSourceURLs.image();
const imageData = await augmentedWithDimensions(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 SlideData}.
*/
const augmentedWithDimensions = (imageURL: string): Promise<SlideData> =>
new Promise((resolve) => {
let image = new Image();
image.onload = () => {
resolve({
src: imageURL,
width: image.naturalWidth,
height: image.naturalHeight,
});
};
// TODO(PS): Handle imageElement.onerror
image.src = imageURL;
});

const videoHTML = (url: string, disableDownload: boolean) => `
<video controls ${disableDownload && "controlsList=nodownload"} oncontextmenu="return false;">
<source src="${url}" />
Expand Down

0 comments on commit 9d76d93

Please sign in to comment.