Singleton => module

This commit is contained in:
Manav Rathi
2025-02-18 09:13:10 +05:30
parent d398838742
commit fe5146ead8
2 changed files with 130 additions and 125 deletions

View File

@@ -38,128 +38,141 @@ type ItemData = SlideData & {
};
/**
* A class that stores and serves data required by our custom PhotoSwipe
* This module 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.
*
* This will be cleared on logout.
*/
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;
}
}
}
class FileViewerDataSourceState {
itemDataByFileID = new Map<number, ItemData>();
needsRefreshByFileID = new Map<number, () => void>();
}
/**
* State shared by functions in this module.
*
* See {@link FileViewerDataSourceState}.
*/
let _state = new FileViewerDataSourceState();
/**
* Clear any internal state maintained by the file viewer data source.
*/
// TODO(PS): Call me during logout sequence once this is integrated.
export const logoutFileViewerDataSource = () => {
_state = new FileViewerDataSourceState();
};
/**
* 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.
*/
export const itemDataForFile = (file: EnteFile, needsRefresh: () => void) => {
let itemData = _state.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.
_state.needsRefreshByFileID.set(file.id, needsRefresh);
if (!itemData) {
itemData = {};
_state.itemDataByFileID.set(file.id, itemData);
void enqueueUpdates(file);
}
return itemData;
};
const enqueueUpdates = async (file: EnteFile) => {
const update = (itemData: ItemData) => {
_state.itemDataByFileID.set(file.id, itemData);
_state.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

View File

@@ -4,7 +4,7 @@
import log from "@/base/log";
import type { EnteFile } from "@/media/file";
import { t } from "i18next";
import { FileViewerDataSource } from "./data-source";
import { itemDataForFile } from "./data-source";
import type { FileViewerProps } from "./FileViewer";
import { createPSRegisterElementIconHTML } from "./icons";
@@ -68,13 +68,6 @@ export class FileViewerPhotoSwipe {
* The options with which we were initialized.
*/
private opts: Pick<FileViewerPhotoSwipeOptions, "disableDownload">;
/**
* Our data source.
*
* TODO(PS): Move this elsewhere, or merge with download manager.
*/
private dataSource: FileViewerDataSource;
/**
* An interval that invokes a periodic check of whether we should the hide
* controls if the user does not perform any pointer events for a while.
@@ -103,7 +96,6 @@ export class FileViewerPhotoSwipe {
}: FileViewerPhotoSwipeOptions) {
this.files = files;
this.opts = { disableDownload };
this.dataSource = new FileViewerDataSource();
const pswp = new PhotoSwipe({
// Opaque background.
@@ -163,7 +155,7 @@ export class FileViewerPhotoSwipe {
pswp.addFilter("itemData", (_, index) => {
const file = files[index]!;
let itemData = this.dataSource.itemDataForFile(file, () => {
let itemData = itemDataForFile(file, () => {
this.pswp.refreshSlideContent(index);
});