Merge pull request #716 from ente-io/thumbnail-scroll

Thumbnail scroll
This commit is contained in:
Neeraj Gupta 2022-09-22 14:18:46 +05:30 committed by GitHub
commit a3c7611633
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 78 additions and 56 deletions

View file

@ -426,7 +426,11 @@ const PhotoFrame = ({
handleSelect(filteredData[index].id, index)(!checked); handleSelect(filteredData[index].id, index)(!checked);
} }
}; };
const getThumbnail = (files: EnteFile[], index: number) => const getThumbnail = (
files: EnteFile[],
index: number,
isScrolling: boolean
) =>
files[index] ? ( files[index] ? (
<PreviewCard <PreviewCard
key={`tile-${files[index].id}-selected-${ key={`tile-${files[index].id}-selected-${
@ -450,6 +454,7 @@ const PhotoFrame = ({
(index >= currentHover && index <= rangeStart) (index >= currentHover && index <= rangeStart)
} }
activeCollection={activeCollection} activeCollection={activeCollection}
showPlaceholder={isScrolling}
/> />
) : ( ) : (
<></> <></>

View file

@ -24,7 +24,6 @@ import { GalleryContext } from 'pages/gallery';
import { SpecialPadding } from 'styles/SpecialPadding'; import { SpecialPadding } from 'styles/SpecialPadding';
const A_DAY = 24 * 60 * 60 * 1000; const A_DAY = 24 * 60 * 60 * 1000;
const NO_OF_PAGES = 2;
const FOOTER_HEIGHT = 90; const FOOTER_HEIGHT = 90;
export enum ITEM_TYPE { export enum ITEM_TYPE {
@ -153,7 +152,11 @@ interface Props {
width: number; width: number;
filteredData: EnteFile[]; filteredData: EnteFile[];
showAppDownloadBanner: boolean; showAppDownloadBanner: boolean;
getThumbnail: (files: EnteFile[], index: number) => JSX.Element; getThumbnail: (
files: EnteFile[],
index: number,
isScrolling?: boolean
) => JSX.Element;
activeCollection: number; activeCollection: number;
resetFetching: () => void; resetFetching: () => void;
} }
@ -512,10 +515,6 @@ export function PhotoList({
} }
}; };
const extraRowsToRender = Math.ceil(
(NO_OF_PAGES * height) / IMAGE_CONTAINER_MAX_HEIGHT
);
const generateKey = (index) => { const generateKey = (index) => {
switch (timeStampList[index].itemType) { switch (timeStampList[index].itemType) {
case ITEM_TYPE.FILE: case ITEM_TYPE.FILE:
@ -527,7 +526,10 @@ export function PhotoList({
} }
}; };
const renderListItem = (listItem: TimeStampListItem) => { const renderListItem = (
listItem: TimeStampListItem,
isScrolling: boolean
) => {
switch (listItem.itemType) { switch (listItem.itemType) {
case ITEM_TYPE.TIME: case ITEM_TYPE.TIME:
return listItem.dates ? ( return listItem.dates ? (
@ -556,7 +558,8 @@ export function PhotoList({
const ret = listItem.items.map((item, idx) => const ret = listItem.items.map((item, idx) =>
getThumbnail( getThumbnail(
filteredDataCopy, filteredDataCopy,
listItem.itemStartIndex + idx listItem.itemStartIndex + idx,
isScrolling
) )
); );
if (listItem.groups) { if (listItem.groups) {
@ -587,14 +590,15 @@ export function PhotoList({
width={width} width={width}
itemCount={timeStampList.length} itemCount={timeStampList.length}
itemKey={generateKey} itemKey={generateKey}
overscanCount={extraRowsToRender}> overscanCount={0}
{({ index, style }) => ( useIsScrolling>
{({ index, style, isScrolling }) => (
<ListItem style={style}> <ListItem style={style}>
<ListContainer <ListContainer
columns={columns} columns={columns}
shrinkRatio={shrinkRatio} shrinkRatio={shrinkRatio}
groups={timeStampList[index].groups}> groups={timeStampList[index].groups}>
{renderListItem(timeStampList[index])} {renderListItem(timeStampList[index], isScrolling)}
</ListContainer> </ListContainer>
</ListItem> </ListItem>
)} )}

View file

@ -1,4 +1,4 @@
import React, { useContext, useLayoutEffect, useRef, useState } from 'react'; import React, { useContext, useLayoutEffect, useState } from 'react';
import { EnteFile } from 'types/file'; import { EnteFile } from 'types/file';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import PlayCircleOutlineOutlinedIcon from '@mui/icons-material/PlayCircleOutlineOutlined'; import PlayCircleOutlineOutlinedIcon from '@mui/icons-material/PlayCircleOutlineOutlined';
@ -18,18 +18,18 @@ import { formatDateRelative } from 'utils/time';
interface IProps { interface IProps {
file: EnteFile; file: EnteFile;
updateURL?: (url: string) => EnteFile; updateURL: (url: string) => EnteFile;
onClick?: () => void; onClick: () => void;
forcedEnable?: boolean; selectable: boolean;
selectable?: boolean; selected: boolean;
selected?: boolean; onSelect: (checked: boolean) => void;
onSelect?: (checked: boolean) => void; onHover: () => void;
onHover?: () => void; onRangeSelect: () => void;
onRangeSelect?: () => void; isRangeSelectActive: boolean;
isRangeSelectActive?: boolean; selectOnClick: boolean;
selectOnClick?: boolean; isInsSelectRange: boolean;
isInsSelectRange?: boolean; activeCollection: number;
activeCollection?: number; showPlaceholder: boolean;
} }
const Check = styled('input')<{ active: boolean }>` const Check = styled('input')<{ active: boolean }>`
@ -203,7 +203,6 @@ export default function PreviewCard(props: IProps) {
file, file,
onClick, onClick,
updateURL, updateURL,
forcedEnable,
selectable, selectable,
selected, selected,
onSelect, onSelect,
@ -213,14 +212,13 @@ export default function PreviewCard(props: IProps) {
isRangeSelectActive, isRangeSelectActive,
isInsSelectRange, isInsSelectRange,
} = props; } = props;
const isMounted = useRef(true);
const publicCollectionGalleryContext = useContext( const publicCollectionGalleryContext = useContext(
PublicCollectionGalleryContext PublicCollectionGalleryContext
); );
const deduplicateContext = useContext(DeduplicateContext); const deduplicateContext = useContext(DeduplicateContext);
useLayoutEffect(() => { useLayoutEffect(() => {
if (file && !file.msrc) { if (file && !file.msrc && !props.showPlaceholder) {
const main = async () => { const main = async () => {
try { try {
let url; let url;
@ -236,18 +234,14 @@ export default function PreviewCard(props: IProps) {
} else { } else {
url = await DownloadManager.getThumbnail(file); url = await DownloadManager.getThumbnail(file);
} }
if (isMounted.current) { setImgSrc(url);
setImgSrc(url); thumbs.set(file.id, url);
thumbs.set(file.id, url); const newFile = updateURL(url);
if (updateURL) { file.msrc = newFile.msrc;
const newFile = updateURL(url); file.html = newFile.html;
file.msrc = newFile.msrc; file.src = newFile.src;
file.html = newFile.html; file.w = newFile.w;
file.src = newFile.src; file.h = newFile.h;
file.w = newFile.w;
file.h = newFile.h;
}
}
} catch (e) { } catch (e) {
logError(e, 'preview card useEffect failed'); logError(e, 'preview card useEffect failed');
// no-op // no-op
@ -262,12 +256,7 @@ export default function PreviewCard(props: IProps) {
main(); main();
} }
} }
}, [file, props.showPlaceholder]);
return () => {
// cool cool cool
isMounted.current = false;
};
}, [file]);
const handleClick = () => { const handleClick = () => {
if (selectOnClick) { if (selectOnClick) {
@ -300,10 +289,10 @@ export default function PreviewCard(props: IProps) {
return ( return (
<Cont <Cont
id={`thumb-${file?.id}`} id={`thumb-${file?.id}-${props.showPlaceholder}`}
onClick={handleClick} onClick={handleClick}
onMouseEnter={handleHover} onMouseEnter={handleHover}
disabled={!forcedEnable && !file?.msrc && !imgSrc} disabled={!file?.msrc && !imgSrc}
{...(selectable ? useLongPress(longPressCallback, 500) : {})}> {...(selectable ? useLongPress(longPressCallback, 500) : {})}>
{selectable && ( {selectable && (
<Check <Check

View file

@ -13,11 +13,19 @@ import { logError } from 'utils/sentry';
import { FILE_TYPE } from 'constants/file'; import { FILE_TYPE } from 'constants/file';
import { CustomError } from 'utils/error'; import { CustomError } from 'utils/error';
import { openThumbnailCache } from './cacheService'; import { openThumbnailCache } from './cacheService';
import QueueProcessor, { PROCESSING_STRATEGY } from './queueProcessor';
const MAX_PARALLEL_DOWNLOADS = 10;
class DownloadManager { class DownloadManager {
private fileObjectURLPromise = new Map<string, Promise<string[]>>(); private fileObjectURLPromise = new Map<string, Promise<string[]>>();
private thumbnailObjectURLPromise = new Map<number, Promise<string>>(); private thumbnailObjectURLPromise = new Map<number, Promise<string>>();
private thumbnailDownloadRequestsProcessor = new QueueProcessor<any>(
MAX_PARALLEL_DOWNLOADS,
PROCESSING_STRATEGY.LIFO
);
public async getThumbnail(file: EnteFile) { public async getThumbnail(file: EnteFile) {
try { try {
const token = getToken(); const token = getToken();
@ -34,7 +42,10 @@ class DownloadManager {
if (cacheResp) { if (cacheResp) {
return URL.createObjectURL(await cacheResp.blob()); return URL.createObjectURL(await cacheResp.blob());
} }
const thumb = await this.downloadThumb(token, file); const thumb =
await this.thumbnailDownloadRequestsProcessor.queueUpRequest(
() => this.downloadThumb(token, file)
).promise;
const thumbBlob = new Blob([thumb]); const thumbBlob = new Blob([thumb]);
thumbnailCache thumbnailCache

View file

@ -14,11 +14,14 @@ import { EnteFile } from 'types/file';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { FILE_TYPE } from 'constants/file'; import { FILE_TYPE } from 'constants/file';
import { CustomError } from 'utils/error'; import { CustomError } from 'utils/error';
import QueueProcessor from './queueProcessor';
class PublicCollectionDownloadManager { class PublicCollectionDownloadManager {
private fileObjectURLPromise = new Map<string, Promise<string[]>>(); private fileObjectURLPromise = new Map<string, Promise<string[]>>();
private thumbnailObjectURLPromise = new Map<number, Promise<string>>(); private thumbnailObjectURLPromise = new Map<number, Promise<string>>();
private thumbnailDownloadRequestsProcessor = new QueueProcessor<any>(5);
public async getThumbnail( public async getThumbnail(
file: EnteFile, file: EnteFile,
token: string, token: string,
@ -46,11 +49,10 @@ class PublicCollectionDownloadManager {
if (cacheResp) { if (cacheResp) {
return URL.createObjectURL(await cacheResp.blob()); return URL.createObjectURL(await cacheResp.blob());
} }
const thumb = await this.downloadThumb( const thumb =
token, await this.thumbnailDownloadRequestsProcessor.queueUpRequest(
passwordToken, () => this.downloadThumb(token, passwordToken, file)
file ).promise;
);
const thumbBlob = new Blob([thumb]); const thumbBlob = new Blob([thumb]);
try { try {
await thumbnailCache?.put( await thumbnailCache?.put(

View file

@ -8,6 +8,11 @@ interface RequestQueueItem {
canceller: { exec: () => void }; canceller: { exec: () => void };
} }
export enum PROCESSING_STRATEGY {
FIFO,
LIFO,
}
export interface RequestCanceller { export interface RequestCanceller {
exec: () => void; exec: () => void;
} }
@ -17,7 +22,10 @@ export default class QueueProcessor<T> {
private requestInProcessing = 0; private requestInProcessing = 0;
constructor(private maxParallelProcesses: number) {} constructor(
private maxParallelProcesses: number,
private processingStrategy = PROCESSING_STRATEGY.FIFO
) {}
public queueUpRequest( public queueUpRequest(
request: (canceller?: RequestCanceller) => Promise<T> request: (canceller?: RequestCanceller) => Promise<T>
@ -52,7 +60,10 @@ export default class QueueProcessor<T> {
private async processQueue() { private async processQueue() {
while (this.requestQueue.length > 0) { while (this.requestQueue.length > 0) {
const queueItem = this.requestQueue.shift(); const queueItem =
this.processingStrategy === PROCESSING_STRATEGY.LIFO
? this.requestQueue.pop()
: this.requestQueue.shift();
let response = null; let response = null;
if (queueItem.isCanceled.status) { if (queueItem.isCanceled.status) {