Download progress (#1357)

This commit is contained in:
Abhinav Kumar 2023-09-14 13:25:31 +05:30 committed by GitHub
commit 7d3c4d85b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 184 additions and 75 deletions

View file

@ -1,12 +0,0 @@
import React from 'react';
export const livePhotoBtnHTML = (
<svg
xmlns="http://www.w3.org/2000/svg"
height="25px"
width="25px"
fill="currentColor">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M6.76 4.84l-1.8-1.79-1.41 1.41 1.79 1.79zM1 10.5h3v2H1zM11 .55h2V3.5h-2zm8.04 2.495l1.408 1.407-1.79 1.79-1.407-1.408zm-1.8 15.115l1.79 1.8 1.41-1.41-1.8-1.79zM20 10.5h3v2h-3zm-8-5c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6zm0 10c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm-1 4h2v2.95h-2zm-7.45-.96l1.41 1.41 1.79-1.8-1.41-1.41z" />
</svg>
);

View file

@ -13,7 +13,6 @@ import { MergedSourceURL, SelectedState } from 'types/gallery';
import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager'; import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager';
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery'; import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { AppContext } from 'pages/_app';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { addLogLine } from 'utils/logging'; import { addLogLine } from 'utils/logging';
import PhotoSwipe from 'photoswipe'; import PhotoSwipe from 'photoswipe';
@ -75,7 +74,6 @@ const PhotoFrame = ({
const [currentIndex, setCurrentIndex] = useState<number>(0); const [currentIndex, setCurrentIndex] = useState<number>(0);
const [fetching, setFetching] = useState<{ [k: number]: boolean }>({}); const [fetching, setFetching] = useState<{ [k: number]: boolean }>({});
const galleryContext = useContext(GalleryContext); const galleryContext = useContext(GalleryContext);
const appContext = useContext(AppContext);
const publicCollectionGalleryContext = useContext( const publicCollectionGalleryContext = useContext(
PublicCollectionGalleryContext PublicCollectionGalleryContext
); );
@ -468,7 +466,6 @@ const PhotoFrame = ({
addLogLine( addLogLine(
`[${item.id}] gallery context cache miss, calling downloadManager to get file` `[${item.id}] gallery context cache miss, calling downloadManager to get file`
); );
appContext.startLoading();
let downloadedURL: { let downloadedURL: {
original: string[]; original: string[];
converted: string[]; converted: string[];
@ -484,7 +481,6 @@ const PhotoFrame = ({
} else { } else {
downloadedURL = await DownloadManager.getFile(item, true); downloadedURL = await DownloadManager.getFile(item, true);
} }
appContext.finishLoading();
const mergedURL: MergedSourceURL = { const mergedURL: MergedSourceURL = {
original: downloadedURL.original.join(','), original: downloadedURL.original.join(','),
converted: downloadedURL.converted.join(','), converted: downloadedURL.converted.join(','),

View file

@ -13,7 +13,6 @@ import {
getFileExtension, getFileExtension,
getFileFromURL, getFileFromURL,
} from 'utils/file'; } from 'utils/file';
import { livePhotoBtnHTML } from 'components/LivePhotoBtn';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { FILE_TYPE } from 'constants/file'; import { FILE_TYPE } from 'constants/file';
@ -26,7 +25,7 @@ import {
defaultLivePhotoDefaultOptions, defaultLivePhotoDefaultOptions,
photoSwipeV4Events, photoSwipeV4Events,
} from 'constants/photoViewer'; } from 'constants/photoViewer';
import { LivePhotoBtn } from './styledComponents/LivePhotoBtn'; import { LivePhotoBtnContainer } from './styledComponents/LivePhotoBtn';
import DownloadIcon from '@mui/icons-material/Download'; import DownloadIcon from '@mui/icons-material/Download';
import InfoIcon from '@mui/icons-material/InfoOutlined'; import InfoIcon from '@mui/icons-material/InfoOutlined';
import FavoriteIcon from '@mui/icons-material/FavoriteRounded'; import FavoriteIcon from '@mui/icons-material/FavoriteRounded';
@ -35,7 +34,7 @@ import ChevronRight from '@mui/icons-material/ChevronRight';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import { trashFiles } from 'services/fileService'; import { trashFiles } from 'services/fileService';
import { getTrashFileMessage } from 'utils/ui'; import { getTrashFileMessage } from 'utils/ui';
import { styled } from '@mui/material'; import { Box, Button, styled } from '@mui/material';
import { addLocalLog } from 'utils/logging'; import { addLocalLog } from 'utils/logging';
import ContentCopy from '@mui/icons-material/ContentCopy'; import ContentCopy from '@mui/icons-material/ContentCopy';
import ChevronLeft from '@mui/icons-material/ChevronLeft'; import ChevronLeft from '@mui/icons-material/ChevronLeft';
@ -44,7 +43,13 @@ import { getParsedExifData } from 'services/upload/exifService';
import { getFileType } from 'services/typeDetectionService'; import { getFileType } from 'services/typeDetectionService';
import { ConversionFailedNotification } from './styledComponents/ConversionFailedNotification'; import { ConversionFailedNotification } from './styledComponents/ConversionFailedNotification';
import { GalleryContext } from 'pages/gallery'; import { GalleryContext } from 'pages/gallery';
import { ConvertBtn } from './styledComponents/ConvertBtn'; import { ConvertBtnContainer } from './styledComponents/ConvertBtn';
import downloadManager from 'services/downloadManager';
import publicCollectionDownloadManager from 'services/publicCollectionDownloadManager';
import CircularProgressWithLabel from './styledComponents/CircularProgressWithLabel';
import EnteSpinner from 'components/EnteSpinner';
import AlbumOutlined from '@mui/icons-material/AlbumOutlined';
import { FlexWrapper } from 'components/Container';
interface PhotoswipeFullscreenAPI { interface PhotoswipeFullscreenAPI {
enter: () => void; enter: () => void;
@ -117,6 +122,20 @@ function PhotoViewer(props: Iprops) {
setConversionFailedNotificationOpen, setConversionFailedNotificationOpen,
] = useState(false); ] = useState(false);
const [fileDownloadProgress, setFileDownloadProgress] = useState<
Map<number, number>
>(new Map());
useEffect(() => {
if (publicCollectionGalleryContext.accessedThroughSharedURL) {
publicCollectionDownloadManager.setProgressUpdater(
setFileDownloadProgress
);
} else {
downloadManager.setProgressUpdater(setFileDownloadProgress);
}
}, []);
useEffect(() => { useEffect(() => {
if (!pswpElement) return; if (!pswpElement) return;
if (isOpen) { if (isOpen) {
@ -599,13 +618,18 @@ function PhotoViewer(props: Iprops) {
<div className="pswp__bg" /> <div className="pswp__bg" />
<div className="pswp__scroll-wrap"> <div className="pswp__scroll-wrap">
{livePhotoBtnOptions.visible && ( {livePhotoBtnOptions.visible && (
<LivePhotoBtn <LivePhotoBtnContainer>
onClick={livePhotoBtnOptions.click} <Button
onMouseEnter={livePhotoBtnOptions.show} color="secondary"
onMouseLeave={livePhotoBtnOptions.hide} onClick={livePhotoBtnOptions.click}
disabled={livePhotoBtnOptions.loading}> onMouseEnter={livePhotoBtnOptions.show}
{livePhotoBtnHTML} {t('LIVE')} onMouseLeave={livePhotoBtnOptions.hide}
</LivePhotoBtn> disabled={livePhotoBtnOptions.loading}>
<FlexWrapper gap={'4px'}>
{<AlbumOutlined />} {t('LIVE')}
</FlexWrapper>
</Button>
</LivePhotoBtnContainer>
)} )}
<ConversionFailedNotification <ConversionFailedNotification
open={conversionFailedNotificationOpen} open={conversionFailedNotificationOpen}
@ -615,11 +639,35 @@ function PhotoViewer(props: Iprops) {
onClick={() => downloadFileHelper(photoSwipe.currItem)} onClick={() => downloadFileHelper(photoSwipe.currItem)}
/> />
{showConvertBtn && ( {showConvertBtn && (
<ConvertBtn onClick={triggerManualConvert}> <ConvertBtnContainer>
{t('CONVERT')} <Button
</ConvertBtn> color="secondary"
onClick={triggerManualConvert}>
{t('CONVERT')}
</Button>
</ConvertBtnContainer>
)} )}
<Box
sx={{
position: 'absolute',
top: '10vh',
right: '2vh',
zIndex: 10,
}}>
{fileDownloadProgress.has(
(photoSwipe?.currItem as EnteFile)?.id
) ? (
<CircularProgressWithLabel
value={fileDownloadProgress.get(
(photoSwipe.currItem as EnteFile)?.id
)}
/>
) : (
!isSourceLoaded && <EnteSpinner />
)}
</Box>
<div className="pswp__container"> <div className="pswp__container">
<div className="pswp__item" /> <div className="pswp__item" />
<div className="pswp__item" /> <div className="pswp__item" />

View file

@ -0,0 +1,32 @@
import {
CircularProgressProps,
CircularProgress,
Typography,
} from '@mui/material';
import { Overlay } from 'components/Container';
function CircularProgressWithLabel(
props: CircularProgressProps & { value: number }
) {
return (
<>
<CircularProgress variant="determinate" {...props} color="accent" />
<Overlay
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '40px',
}}>
<Typography
variant="mini"
component="div"
color="text.secondary">{`${Math.round(
props.value
)}%`}</Typography>
</Overlay>
</>
);
}
export default CircularProgressWithLabel;

View file

@ -1,14 +1,9 @@
import { Button, ButtonProps, styled } from '@mui/material'; import { Paper, styled } from '@mui/material';
export const ConvertBtn = styled((props: ButtonProps) => (
<Button color="secondary" {...props} /> export const ConvertBtnContainer = styled(Paper)`
))` border-radius: 4px;
position: absolute; position: absolute;
bottom: 10vh; bottom: 10vh;
left: 2vh; left: 2vh;
outline: none;
border: none;
border-radius: 10%;
z-index: 10; z-index: 10;
cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};
}
`; `;

View file

@ -1,14 +1,10 @@
import { Button, ButtonProps, styled } from '@mui/material'; import { Paper } from '@mui/material';
export const LivePhotoBtn = styled((props: ButtonProps) => ( import { styled } from '@mui/material/styles';
<Button color="secondary" {...props} />
))` export const LivePhotoBtnContainer = styled(Paper)`
border-radius: 4px;
position: absolute; position: absolute;
bottom: 6vh; bottom: 10vh;
right: 6vh; right: 6vh;
outline: none;
border: none;
border-radius: 10%;
z-index: 10; z-index: 10;
cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};
}
`; `;

View file

@ -27,6 +27,14 @@ class DownloadManager {
>(); >();
private thumbnailObjectURLPromise = new Map<number, Promise<string>>(); private thumbnailObjectURLPromise = new Map<number, Promise<string>>();
private fileDownloadProgress = new Map<number, number>();
private progressUpdater: (value: Map<number, number>) => void;
setProgressUpdater(progressUpdater: (value: Map<number, number>) => void) {
this.progressUpdater = progressUpdater;
}
private async getThumbnailCache() { private async getThumbnailCache() {
try { try {
const thumbnailCache = await CacheStorageService.open( const thumbnailCache = await CacheStorageService.open(
@ -180,6 +188,7 @@ class DownloadManager {
if (!token) { if (!token) {
return null; return null;
} }
const onDownloadProgress = this.trackDownloadProgress(file.id);
if ( if (
file.metadata.fileType === FILE_TYPE.IMAGE || file.metadata.fileType === FILE_TYPE.IMAGE ||
file.metadata.fileType === FILE_TYPE.LIVE_PHOTO file.metadata.fileType === FILE_TYPE.LIVE_PHOTO
@ -189,7 +198,11 @@ class DownloadManager {
getFileURL(file.id), getFileURL(file.id),
null, null,
{ 'X-Auth-Token': token }, { 'X-Auth-Token': token },
{ responseType: 'arraybuffer', timeout } {
responseType: 'arraybuffer',
timeout,
onDownloadProgress,
}
) )
); );
if (typeof resp.data === 'undefined') { if (typeof resp.data === 'undefined') {
@ -226,6 +239,10 @@ class DownloadManager {
}) })
); );
const reader = resp.body.getReader(); const reader = resp.body.getReader();
const contentLength = +resp.headers.get('Content-Length');
let downloadedBytes = 0;
const stream = new ReadableStream({ const stream = new ReadableStream({
async start(controller) { async start(controller) {
try { try {
@ -246,6 +263,11 @@ class DownloadManager {
try { try {
// Is there more data to read? // Is there more data to read?
if (!done) { if (!done) {
downloadedBytes += value.byteLength;
onDownloadProgress({
loaded: downloadedBytes,
total: contentLength,
});
const buffer = new Uint8Array( const buffer = new Uint8Array(
data.byteLength + value.byteLength data.byteLength + value.byteLength
); );
@ -363,6 +385,20 @@ class DownloadManager {
throw e; throw e;
} }
} }
trackDownloadProgress = (fileID: number) => {
return (event: { loaded: number; total: number }) => {
if (event.loaded === event.total) {
this.fileDownloadProgress.delete(fileID);
} else {
this.fileDownloadProgress.set(
fileID,
Math.round((event.loaded * 100) / event.total)
);
}
this.progressUpdater(new Map(this.fileDownloadProgress));
};
};
} }
export default new DownloadManager(); export default new DownloadManager();

View file

@ -25,6 +25,14 @@ class PublicCollectionDownloadManager {
>(); >();
private thumbnailObjectURLPromise = new Map<number, Promise<string>>(); private thumbnailObjectURLPromise = new Map<number, Promise<string>>();
private fileDownloadProgress = new Map<number, number>();
private progressUpdater: (value: Map<number, number>) => void;
setProgressUpdater(progressUpdater: (value: Map<number, number>) => void) {
this.progressUpdater = progressUpdater;
}
private async getThumbnailCache() { private async getThumbnailCache() {
try { try {
const thumbnailCache = await CacheStorageService.open( const thumbnailCache = await CacheStorageService.open(
@ -182,6 +190,8 @@ class PublicCollectionDownloadManager {
if (!token) { if (!token) {
return null; return null;
} }
const onDownloadProgress = this.trackDownloadProgress(file.id);
if ( if (
file.metadata.fileType === FILE_TYPE.IMAGE || file.metadata.fileType === FILE_TYPE.IMAGE ||
file.metadata.fileType === FILE_TYPE.LIVE_PHOTO file.metadata.fileType === FILE_TYPE.LIVE_PHOTO
@ -193,6 +203,7 @@ class PublicCollectionDownloadManager {
'X-Auth-Access-Token': token, 'X-Auth-Access-Token': token,
...(passwordToken && { ...(passwordToken && {
'X-Auth-Access-Token-JWT': passwordToken, 'X-Auth-Access-Token-JWT': passwordToken,
onDownloadProgress,
}), }),
}, },
{ responseType: 'arraybuffer' } { responseType: 'arraybuffer' }
@ -216,6 +227,10 @@ class PublicCollectionDownloadManager {
}, },
}); });
const reader = resp.body.getReader(); const reader = resp.body.getReader();
const contentLength = +resp.headers.get('Content-Length');
let downloadedBytes = 0;
const stream = new ReadableStream({ const stream = new ReadableStream({
async start(controller) { async start(controller) {
const decryptionHeader = await cryptoWorker.fromB64( const decryptionHeader = await cryptoWorker.fromB64(
@ -234,6 +249,11 @@ class PublicCollectionDownloadManager {
reader.read().then(async ({ done, value }) => { reader.read().then(async ({ done, value }) => {
// Is there more data to read? // Is there more data to read?
if (!done) { if (!done) {
downloadedBytes += value.byteLength;
onDownloadProgress({
loaded: downloadedBytes,
total: contentLength,
});
const buffer = new Uint8Array( const buffer = new Uint8Array(
data.byteLength + value.byteLength data.byteLength + value.byteLength
); );
@ -275,6 +295,20 @@ class PublicCollectionDownloadManager {
}); });
return stream; return stream;
} }
trackDownloadProgress = (fileID: number) => {
return (event: { loaded: number; total: number }) => {
if (event.loaded === event.total) {
this.fileDownloadProgress.delete(fileID);
} else {
this.fileDownloadProgress.set(
fileID,
Math.round((event.loaded * 100) / event.total)
);
}
this.progressUpdater(new Map(this.fileDownloadProgress));
};
};
} }
export default new PublicCollectionDownloadManager(); export default new PublicCollectionDownloadManager();

View file

@ -43,32 +43,16 @@ export function updateFileMsrcProps(file: EnteFile, url: string) {
file.isSourceLoaded = false; file.isSourceLoaded = false;
file.conversionFailed = false; file.conversionFailed = false;
file.isConverted = false; file.isConverted = false;
if (file.metadata.fileType === FILE_TYPE.VIDEO) { file.src = null;
file.html = ` file.html = null;
<div class="pswp-item-container"> if (file.metadata.fileType === FILE_TYPE.IMAGE) {
<img src="${url}" onContextMenu="return false;"/>
<div class="spinner-border text-light" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
`;
} else if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
file.html = `
<div class="pswp-item-container">
<img src="${url}" onContextMenu="return false;"/>
<div class="spinner-border text-light" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
`;
} else if (file.metadata.fileType === FILE_TYPE.IMAGE) {
file.src = url; file.src = url;
} else { } else {
logError( file.html = `
Error(`unknown file type - ${file.metadata.fileType}`), <div class = 'pswp-item-container'>
'Unknown file type' <img src="${url}"/>
); </div>
file.src = url; `;
} }
} }
@ -119,9 +103,9 @@ export async function updateFileSrcProps(
file.originalVideoURL = originalVideoURL; file.originalVideoURL = originalVideoURL;
file.isConverted = isConverted; file.isConverted = isConverted;
file.conversionFailed = conversionFailed; file.conversionFailed = conversionFailed;
file.src = null;
if (!isPlayable) { if (!isPlayable) {
file.src = file.msrc;
return; return;
} }