diff --git a/web/apps/photos/src/styles/global.css b/web/apps/photos/src/styles/global.css index b2dc418309..3eb8dd8d62 100644 --- a/web/apps/photos/src/styles/global.css +++ b/web/apps/photos/src/styles/global.css @@ -147,15 +147,12 @@ body { overflow: hidden; width: 50px; height: 60px; - /* "visibility" is used to toggle visibility, opacity is fixed to be similar - to that of the loading indicator when it is visible. */ - opacity: 0.5; + /* Unlike the loading indicator, "display" is used to toggle visibility, and + the opacity is fixed to be similar to that of the counter. */ + opacity: 0.85; + display: none; } -.pswp-ente .pswp__error .pswp__icn { - visibility: hidden; -} - -.pswp-ente .pswp__error--active .pswp__icn { - visibility: visible; +.pswp-ente .pswp__error--active { + display: initial; } diff --git a/web/packages/gallery/components/viewer/data-source.ts b/web/packages/gallery/components/viewer/data-source.ts index 2bd0f0a841..0abaf0755a 100644 --- a/web/packages/gallery/components/viewer/data-source.ts +++ b/web/packages/gallery/components/viewer/data-source.ts @@ -154,7 +154,7 @@ export const itemDataForFile = (file: EnteFile, needsRefresh: () => void) => { // point of time. This assumption is currently valid. _state.needsRefreshByFileID.set(file.id, needsRefresh); - if (!itemData || itemData.failureReason) { + if (!itemData) { itemData = {}; _state.itemDataByFileID.set(file.id, itemData); void enqueueUpdates(file); @@ -163,24 +163,37 @@ export const itemDataForFile = (file: EnteFile, needsRefresh: () => void) => { return itemData; }; +/** + * Reset any failure reasons for the given {@link file}. + * + * This is called when the user moves away from a slide, so that when the come + * back the next time, the entire process is retried. + */ +export const resetFailuresForFile = (file: EnteFile) => { + if (_state.itemDataByFileID.get(file.id)?.failureReason) { + _state.itemDataByFileID.delete(file.id); + } +}; + const enqueueUpdates = async (file: EnteFile) => { const update = (itemData: ItemData) => { _state.itemDataByFileID.set(file.id, itemData); _state.needsRefreshByFileID.get(file.id)?.(); }; + let thumbnailData: ItemData; try { const thumbnailURL = await downloadManager.renderableThumbnailURL(file); // TODO(PS): - const thumbnailData = await withDimensions(thumbnailURL!); + thumbnailData = await withDimensions(thumbnailURL!); update({ ...thumbnailData, isContentLoading: true, isContentZoomable: false, }); } catch (e) { - // If we can't even get the thumbnail, then a persistent (for now) - // network error is likely (download manager already has retries). + // If we can't even get the thumbnail, then a network error is likely + // (download manager already has retries). // // Notify the user of the error. The entire process will be retried when // they reopen the slide later. @@ -189,33 +202,42 @@ const enqueueUpdates = async (file: EnteFile) => { return; } - 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; - } + try { + 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.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; + 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; + } } + } catch (e) { + log.error("Failed to show file", e); + update({ ...thumbnailData, failureReason: "other" }); } }; diff --git a/web/packages/gallery/components/viewer/icons.tsx b/web/packages/gallery/components/viewer/icons.tsx index a021afb96c..5b8201f459 100644 --- a/web/packages/gallery/components/viewer/icons.tsx +++ b/web/packages/gallery/components/viewer/icons.tsx @@ -20,7 +20,7 @@ const paths = { // TODO(PS): This transform is temporary, audit later. info: ' ({ isCustomSVG: true, - inner: `${paths[name]} id="pswp__icn-${name}"`, + inner: `${paths[name]} id="pswp__icn-${name}" />`, outlineID: `pswp__icn-${name}`, }); diff --git a/web/packages/gallery/components/viewer/photoswipe.ts b/web/packages/gallery/components/viewer/photoswipe.ts index ffe82140b4..8684ad7c21 100644 --- a/web/packages/gallery/components/viewer/photoswipe.ts +++ b/web/packages/gallery/components/viewer/photoswipe.ts @@ -4,7 +4,7 @@ import log from "@/base/log"; import type { EnteFile } from "@/media/file"; import { t } from "i18next"; -import { itemDataForFile } from "./data-source"; +import { itemDataForFile, resetFailuresForFile } from "./data-source"; import type { FileViewerProps } from "./FileViewer"; import { createPSRegisterElementIconHTML } from "./icons"; @@ -145,6 +145,11 @@ export class FileViewerPhotoSwipe { mainClass: "pswp-ente", }); + // Helper routines to obtain the file at `currIndex`. + const currentFile = () => this.files[pswp.currIndex]!; + const withCurrentFile = (cb: (file: EnteFile) => void) => () => + cb(currentFile()); + // Provide data about slides to PhotoSwipe via callbacks // https://photoswipe.com/data-sources/#dynamically-generated-data @@ -234,15 +239,24 @@ export class FileViewerPhotoSwipe { }); pswp.on("contentDeactivate", (e) => { - // Pause the video element (if any) on a slide when we move away - // from it. + // Reset failures, if any, for this file so that the fetch is tried + // again when we come back to it^. + // + // ^ Note that because of how the preloading works, this will have + // an effect (i.e. the retry will happen) only if the user moves + // more than 2 slides and then back, or if they reopen the viewer. + resetFailuresForFile(currentFile()); + + // Pause the video element, if any, when we move away from the + // slide. const video = e.content?.slide?.container?.getElementsByTagName("video")[0]; video?.pause(); }); pswp.on("contentActivate", (e) => { - // Undo the effect of a previous "contentDeactivate". + // Undo the effect of a previous "contentDeactivate" if it was + // displaying a live photo. if (e.content?.slide.data?.livePhotoVideoURL) { e.content?.slide?.container ?.getElementsByTagName("video")[0] @@ -258,9 +272,6 @@ export class FileViewerPhotoSwipe { onClose(); }); - const withCurrentFile = (cb: (file: EnteFile) => void) => () => - cb(this.files[this.pswp.currIndex]!); - // Add our custom UI elements to inside the PhotoSwipe dialog. // // API docs for registerElement: @@ -277,35 +288,12 @@ export class FileViewerPhotoSwipe { order: 6, html: createPSRegisterElementIconHTML("error"), onInit: (errorElement, pswp) => { - let isVisible = false; - - const toggleIndicatorClass = (className, add) => { + pswp.on("change", () => { errorElement.classList.toggle( - "pswp__error--" + className, - add, + "pswp__error--active", + !!pswp.currSlide.content.data.failureReason, ); - }; - - const setIndicatorVisibility = (visible) => { - if (isVisible !== visible) { - isVisible = visible; - toggleIndicatorClass("active", visible); - } - }; - - const updateErrorIndicatorVisibility = () => { - console.log( - "updateErrorIndicatorVisibility", - pswp.currSlide.content, - ); - if (!pswp.currSlide.content.data.failureReason) { - setIndicatorVisibility(true); - } else { - setIndicatorVisibility(false); - } - }; - - pswp.on("change", updateErrorIndicatorVisibility); + }); }, }); pswp.ui.registerElement({ @@ -316,23 +304,6 @@ export class FileViewerPhotoSwipe { html: createPSRegisterElementIconHTML("info"), onClick: withCurrentFile(onViewInfo), }); - // const counterIndicator2 = { - // name: "counter-2", - // order: 5, - // html: '
', - - // onInit: (counterElement, pswp) => { - // pswp.on("change", () => { - // counterElement.style.fill = "red"; - // // counterElement.innerText = - // // pswp.currIndex + - // // 1 + - // // pswp.options.indexIndicatorSep + - // // pswp.getNumItems(); - // }); - // }, - // }; - // pswp.ui.registerElement(counterIndicator2); }); // Modify the default UI elements.