Download progress (#1357)
This commit is contained in:
commit
7d3c4d85b9
|
@ -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>
|
||||
);
|
|
@ -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(','),
|
||||
|
|
|
@ -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
|
||||
onClick={livePhotoBtnOptions.click}
|
||||
onMouseEnter={livePhotoBtnOptions.show}
|
||||
onMouseLeave={livePhotoBtnOptions.hide}
|
||||
disabled={livePhotoBtnOptions.loading}>
|
||||
{livePhotoBtnHTML} {t('LIVE')}
|
||||
</LivePhotoBtn>
|
||||
<LivePhotoBtnContainer>
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={livePhotoBtnOptions.click}
|
||||
onMouseEnter={livePhotoBtnOptions.show}
|
||||
onMouseLeave={livePhotoBtnOptions.hide}
|
||||
disabled={livePhotoBtnOptions.loading}>
|
||||
<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}>
|
||||
{t('CONVERT')}
|
||||
</ConvertBtn>
|
||||
<ConvertBtnContainer>
|
||||
<Button
|
||||
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__item" />
|
||||
<div className="pswp__item" />
|
||||
|
|
|
@ -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;
|
|
@ -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')};
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -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')};
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue