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

View file

@ -13,7 +13,6 @@ import {
getFileExtension,
getFileFromURL,
} from 'utils/file';
import { livePhotoBtnHTML } from 'components/LivePhotoBtn';
import { logError } from 'utils/sentry';
import { FILE_TYPE } from 'constants/file';
@ -26,7 +25,7 @@ import {
defaultLivePhotoDefaultOptions,
photoSwipeV4Events,
} from 'constants/photoViewer';
import { LivePhotoBtn } from './styledComponents/LivePhotoBtn';
import { LivePhotoBtnContainer } from './styledComponents/LivePhotoBtn';
import DownloadIcon from '@mui/icons-material/Download';
import InfoIcon from '@mui/icons-material/InfoOutlined';
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 { trashFiles } from 'services/fileService';
import { getTrashFileMessage } from 'utils/ui';
import { styled } from '@mui/material';
import { Box, Button, styled } from '@mui/material';
import { addLocalLog } from 'utils/logging';
import ContentCopy from '@mui/icons-material/ContentCopy';
import ChevronLeft from '@mui/icons-material/ChevronLeft';
@ -44,7 +43,13 @@ import { getParsedExifData } from 'services/upload/exifService';
import { getFileType } from 'services/typeDetectionService';
import { ConversionFailedNotification } from './styledComponents/ConversionFailedNotification';
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 {
enter: () => void;
@ -117,6 +122,20 @@ function PhotoViewer(props: Iprops) {
setConversionFailedNotificationOpen,
] = useState(false);
const [fileDownloadProgress, setFileDownloadProgress] = useState<
Map<number, number>
>(new Map());
useEffect(() => {
if (publicCollectionGalleryContext.accessedThroughSharedURL) {
publicCollectionDownloadManager.setProgressUpdater(
setFileDownloadProgress
);
} else {
downloadManager.setProgressUpdater(setFileDownloadProgress);
}
}, []);
useEffect(() => {
if (!pswpElement) return;
if (isOpen) {
@ -599,13 +618,18 @@ function PhotoViewer(props: Iprops) {
<div className="pswp__bg" />
<div className="pswp__scroll-wrap">
{livePhotoBtnOptions.visible && (
<LivePhotoBtn
<LivePhotoBtnContainer>
<Button
color="secondary"
onClick={livePhotoBtnOptions.click}
onMouseEnter={livePhotoBtnOptions.show}
onMouseLeave={livePhotoBtnOptions.hide}
disabled={livePhotoBtnOptions.loading}>
{livePhotoBtnHTML} {t('LIVE')}
</LivePhotoBtn>
<FlexWrapper gap={'4px'}>
{<AlbumOutlined />} {t('LIVE')}
</FlexWrapper>
</Button>
</LivePhotoBtnContainer>
)}
<ConversionFailedNotification
open={conversionFailedNotificationOpen}
@ -615,11 +639,35 @@ function PhotoViewer(props: Iprops) {
onClick={() => downloadFileHelper(photoSwipe.currItem)}
/>
{showConvertBtn && (
<ConvertBtn onClick={triggerManualConvert}>
<ConvertBtnContainer>
<Button
color="secondary"
onClick={triggerManualConvert}>
{t('CONVERT')}
</ConvertBtn>
</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__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';
export const ConvertBtn = styled((props: ButtonProps) => (
<Button color="secondary" {...props} />
))`
import { Paper, styled } from '@mui/material';
export const ConvertBtnContainer = styled(Paper)`
border-radius: 4px;
position: absolute;
bottom: 10vh;
left: 2vh;
outline: none;
border: none;
border-radius: 10%;
z-index: 10;
cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};
}
`;

View file

@ -1,14 +1,10 @@
import { Button, ButtonProps, styled } from '@mui/material';
export const LivePhotoBtn = styled((props: ButtonProps) => (
<Button color="secondary" {...props} />
))`
import { Paper } from '@mui/material';
import { styled } from '@mui/material/styles';
export const LivePhotoBtnContainer = styled(Paper)`
border-radius: 4px;
position: absolute;
bottom: 6vh;
bottom: 10vh;
right: 6vh;
outline: none;
border: none;
border-radius: 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 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() {
try {
const thumbnailCache = await CacheStorageService.open(
@ -180,6 +188,7 @@ class DownloadManager {
if (!token) {
return null;
}
const onDownloadProgress = this.trackDownloadProgress(file.id);
if (
file.metadata.fileType === FILE_TYPE.IMAGE ||
file.metadata.fileType === FILE_TYPE.LIVE_PHOTO
@ -189,7 +198,11 @@ class DownloadManager {
getFileURL(file.id),
null,
{ 'X-Auth-Token': token },
{ responseType: 'arraybuffer', timeout }
{
responseType: 'arraybuffer',
timeout,
onDownloadProgress,
}
)
);
if (typeof resp.data === 'undefined') {
@ -226,6 +239,10 @@ class DownloadManager {
})
);
const reader = resp.body.getReader();
const contentLength = +resp.headers.get('Content-Length');
let downloadedBytes = 0;
const stream = new ReadableStream({
async start(controller) {
try {
@ -246,6 +263,11 @@ class DownloadManager {
try {
// Is there more data to read?
if (!done) {
downloadedBytes += value.byteLength;
onDownloadProgress({
loaded: downloadedBytes,
total: contentLength,
});
const buffer = new Uint8Array(
data.byteLength + value.byteLength
);
@ -363,6 +385,20 @@ class DownloadManager {
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();

View file

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

View file

@ -43,32 +43,16 @@ export function updateFileMsrcProps(file: EnteFile, url: string) {
file.isSourceLoaded = false;
file.conversionFailed = false;
file.isConverted = false;
if (file.metadata.fileType === FILE_TYPE.VIDEO) {
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.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 = null;
file.html = null;
if (file.metadata.fileType === FILE_TYPE.IMAGE) {
file.src = url;
} else {
logError(
Error(`unknown file type - ${file.metadata.fileType}`),
'Unknown file type'
);
file.src = url;
file.html = `
<div class = 'pswp-item-container'>
<img src="${url}"/>
</div>
`;
}
}
@ -119,9 +103,9 @@ export async function updateFileSrcProps(
file.originalVideoURL = originalVideoURL;
file.isConverted = isConverted;
file.conversionFailed = conversionFailed;
file.src = null;
if (!isPlayable) {
file.src = file.msrc;
return;
}