commit
dcef6cda39
|
@ -354,7 +354,7 @@ const PhotoFrame = ({
|
|||
if (galleryContext.thumbs.has(item.id)) {
|
||||
url = galleryContext.thumbs.get(item.id);
|
||||
} else {
|
||||
url = await DownloadManager.getPreview(item);
|
||||
url = await DownloadManager.getThumbnail(item);
|
||||
galleryContext.thumbs.set(item.id, url);
|
||||
}
|
||||
updateUrl(item.dataIndex)(url);
|
||||
|
|
25
src/components/icons/DownloadIcon.tsx
Normal file
25
src/components/icons/DownloadIcon.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function DownloadIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={props.height}
|
||||
viewBox={props.viewBox}
|
||||
width={props.width}
|
||||
fill="currentColor">
|
||||
<g>
|
||||
<rect fill="none" height="24" width="24" />
|
||||
</g>
|
||||
<g>
|
||||
<path d="M5,20h14v-2H5V20z M19,9h-4V3H9v6H5l7,7L19,9z" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
DownloadIcon.defaultProps = {
|
||||
height: 24,
|
||||
width: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
};
|
|
@ -177,7 +177,7 @@ export default function PreviewCard(props: IProps) {
|
|||
if (file && !file.msrc) {
|
||||
const main = async () => {
|
||||
try {
|
||||
const url = await DownloadManager.getPreview(file);
|
||||
const url = await DownloadManager.getThumbnail(file);
|
||||
if (isMounted.current) {
|
||||
setImgSrc(url);
|
||||
thumbs.set(file.id, url);
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
FIX_CREATION_TIME_VISIBLE_TO_USER_IDS,
|
||||
User,
|
||||
} from 'services/userService';
|
||||
import DownloadIcon from 'components/icons/DownloadIcon';
|
||||
|
||||
interface Props {
|
||||
addToCollectionHelper: (collection: Collection) => void;
|
||||
|
@ -34,6 +35,7 @@ interface Props {
|
|||
deleteFileHelper: (permanent?: boolean) => void;
|
||||
removeFromCollectionHelper: () => void;
|
||||
fixTimeHelper: () => void;
|
||||
downloadHelper: () => void;
|
||||
count: number;
|
||||
clearSelection: () => void;
|
||||
archiveFilesHelper: () => void;
|
||||
|
@ -79,6 +81,7 @@ const SelectedFileOptions = ({
|
|||
setDialogMessage,
|
||||
setCollectionSelectorAttributes,
|
||||
deleteFileHelper,
|
||||
downloadHelper,
|
||||
count,
|
||||
clearSelection,
|
||||
archiveFilesHelper,
|
||||
|
@ -190,6 +193,11 @@ const SelectedFileOptions = ({
|
|||
</IconButton>
|
||||
</IconWithMessage>
|
||||
)}
|
||||
<IconWithMessage message={constants.DOWNLOAD}>
|
||||
<IconButton onClick={downloadHelper}>
|
||||
<DownloadIcon />
|
||||
</IconButton>
|
||||
</IconWithMessage>
|
||||
<IconWithMessage message={constants.ADD}>
|
||||
<IconButton onClick={addToCollection}>
|
||||
<AddIcon />
|
||||
|
|
|
@ -50,6 +50,7 @@ import { LoadingOverlay } from 'components/LoadingOverlay';
|
|||
import PhotoFrame from 'components/PhotoFrame';
|
||||
import {
|
||||
changeFilesVisibility,
|
||||
downloadFiles,
|
||||
getNonTrashedUniqueUserFiles,
|
||||
getSelectedFiles,
|
||||
mergeMetadata,
|
||||
|
@ -545,6 +546,14 @@ export default function Gallery() {
|
|||
clearSelection();
|
||||
};
|
||||
|
||||
const downloadHelper = async () => {
|
||||
const selectedFiles = getSelectedFiles(selected, files);
|
||||
clearSelection();
|
||||
!syncInProgress.current && loadingBar.current?.continuousStart();
|
||||
await downloadFiles(selectedFiles);
|
||||
!syncInProgress.current && loadingBar.current.complete();
|
||||
};
|
||||
|
||||
return (
|
||||
<GalleryContext.Provider
|
||||
value={{
|
||||
|
@ -714,6 +723,7 @@ export default function Gallery() {
|
|||
)
|
||||
}
|
||||
fixTimeHelper={fixTimeHelper}
|
||||
downloadHelper={downloadHelper}
|
||||
count={selected.count}
|
||||
clearSelection={clearSelection}
|
||||
activeCollection={activeCollection}
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { getToken } from 'utils/common/key';
|
||||
import { getFileUrl, getThumbnailUrl } from 'utils/common/apiUtil';
|
||||
import CryptoWorker from 'utils/crypto';
|
||||
import { generateStreamFromArrayBuffer, convertForPreview } from 'utils/file';
|
||||
import {
|
||||
generateStreamFromArrayBuffer,
|
||||
convertForPreview,
|
||||
needsConversionForPreview,
|
||||
} from 'utils/file';
|
||||
import HTTPService from './HTTPService';
|
||||
import { File, FILE_TYPE } from './fileService';
|
||||
import { logError } from 'utils/sentry';
|
||||
|
@ -10,12 +14,14 @@ class DownloadManager {
|
|||
private fileObjectUrlPromise = new Map<string, Promise<string>>();
|
||||
private thumbnailObjectUrlPromise = new Map<number, Promise<string>>();
|
||||
|
||||
public async getPreview(file: File) {
|
||||
public async getThumbnail(file: File) {
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
if (!this.thumbnailObjectUrlPromise.get(file.id)) {
|
||||
const downloadPromise = async () => {
|
||||
const thumbnailCache = await caches.open('thumbs');
|
||||
const cacheResp: Response = await thumbnailCache.match(
|
||||
file.id.toString()
|
||||
|
@ -23,14 +29,21 @@ class DownloadManager {
|
|||
if (cacheResp) {
|
||||
return URL.createObjectURL(await cacheResp.blob());
|
||||
}
|
||||
if (!this.thumbnailObjectUrlPromise.get(file.id)) {
|
||||
const downloadPromise = this.downloadThumb(
|
||||
token,
|
||||
thumbnailCache,
|
||||
file
|
||||
const thumb = await this.downloadThumb(token, file);
|
||||
const thumbBlob = new Blob([thumb]);
|
||||
try {
|
||||
await thumbnailCache.put(
|
||||
file.id.toString(),
|
||||
new Response(thumbBlob)
|
||||
);
|
||||
this.thumbnailObjectUrlPromise.set(file.id, downloadPromise);
|
||||
} catch (e) {
|
||||
// TODO: handle storage full exception.
|
||||
}
|
||||
return URL.createObjectURL(thumbBlob);
|
||||
};
|
||||
this.thumbnailObjectUrlPromise.set(file.id, downloadPromise());
|
||||
}
|
||||
|
||||
return await this.thumbnailObjectUrlPromise.get(file.id);
|
||||
} catch (e) {
|
||||
this.thumbnailObjectUrlPromise.delete(file.id);
|
||||
|
@ -39,24 +52,7 @@ class DownloadManager {
|
|||
}
|
||||
}
|
||||
|
||||
private downloadThumb = async (
|
||||
token: string,
|
||||
thumbnailCache: Cache,
|
||||
file: File
|
||||
) => {
|
||||
const thumb = await this.getThumbnail(token, file);
|
||||
try {
|
||||
await thumbnailCache.put(
|
||||
file.id.toString(),
|
||||
new Response(new Blob([thumb]))
|
||||
);
|
||||
} catch (e) {
|
||||
// TODO: handle storage full exception.
|
||||
}
|
||||
return URL.createObjectURL(new Blob([thumb]));
|
||||
};
|
||||
|
||||
getThumbnail = async (token: string, file: File) => {
|
||||
downloadThumb = async (token: string, file: File) => {
|
||||
const resp = await HTTPService.get(
|
||||
getThumbnailUrl(file.id),
|
||||
null,
|
||||
|
@ -73,32 +69,38 @@ class DownloadManager {
|
|||
};
|
||||
|
||||
getFile = async (file: File, forPreview = false) => {
|
||||
let fileUID: string;
|
||||
if (file.metadata.fileType === FILE_TYPE.VIDEO) {
|
||||
fileUID = file.id.toString();
|
||||
} else {
|
||||
fileUID = `${file.id}_forPreview=${forPreview}`;
|
||||
}
|
||||
const shouldBeConverted = forPreview && needsConversionForPreview(file);
|
||||
const fileKey = shouldBeConverted
|
||||
? `${file.id}_converted`
|
||||
: `${file.id}`;
|
||||
try {
|
||||
const getFilePromise = async () => {
|
||||
const getFilePromise = async (convert: boolean) => {
|
||||
const fileStream = await this.downloadFile(file);
|
||||
let fileBlob = await new Response(fileStream).blob();
|
||||
if (forPreview) {
|
||||
if (convert) {
|
||||
fileBlob = await convertForPreview(file, fileBlob);
|
||||
}
|
||||
return URL.createObjectURL(fileBlob);
|
||||
};
|
||||
if (!this.fileObjectUrlPromise.get(fileUID)) {
|
||||
this.fileObjectUrlPromise.set(fileUID, getFilePromise());
|
||||
if (!this.fileObjectUrlPromise.get(fileKey)) {
|
||||
this.fileObjectUrlPromise.set(
|
||||
fileKey,
|
||||
getFilePromise(shouldBeConverted)
|
||||
);
|
||||
}
|
||||
return await this.fileObjectUrlPromise.get(fileUID);
|
||||
const fileURL = await this.fileObjectUrlPromise.get(fileKey);
|
||||
return fileURL;
|
||||
} catch (e) {
|
||||
this.fileObjectUrlPromise.delete(fileUID);
|
||||
this.fileObjectUrlPromise.delete(fileKey);
|
||||
logError(e, 'Failed to get File');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
public async getCachedOriginalFile(file: File) {
|
||||
return await this.fileObjectUrlPromise.get(file.id.toString());
|
||||
}
|
||||
|
||||
async downloadFile(file: File) {
|
||||
const worker = await new CryptoWorker();
|
||||
const token = getToken();
|
||||
|
|
|
@ -67,7 +67,7 @@ export async function replaceThumbnail(
|
|||
current: idx,
|
||||
total: largeThumbnailFiles.length,
|
||||
});
|
||||
const originalThumbnail = await downloadManager.getThumbnail(
|
||||
const originalThumbnail = await downloadManager.downloadThumb(
|
||||
token,
|
||||
file
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@ import { logError } from 'utils/sentry';
|
|||
import { BLACK_THUMBNAIL_BASE64 } from '../../../public/images/black-thumbnail-b64';
|
||||
import FFmpegService from 'services/ffmpegService';
|
||||
import { convertToHumanReadable } from 'utils/billingUtil';
|
||||
import { fileIsHEIC } from 'utils/file';
|
||||
import { isFileHEIC } from 'utils/file';
|
||||
import { FileTypeInfo } from './readFileService';
|
||||
|
||||
const MAX_THUMBNAIL_DIMENSION = 720;
|
||||
|
@ -31,7 +31,7 @@ export async function generateThumbnail(
|
|||
let thumbnail: Uint8Array;
|
||||
try {
|
||||
if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) {
|
||||
const isHEIC = fileIsHEIC(fileTypeInfo.exactType);
|
||||
const isHEIC = isFileHEIC(fileTypeInfo.exactType);
|
||||
canvas = await generateImageThumbnail(worker, file, isHEIC);
|
||||
} else {
|
||||
try {
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from 'services/fileService';
|
||||
import { decodeMotionPhoto } from 'services/motionPhotoService';
|
||||
import { getMimeTypeFromBlob } from 'services/upload/readFileService';
|
||||
import DownloadManger from 'services/downloadManager';
|
||||
import DownloadManager from 'services/downloadManager';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { User } from 'services/userService';
|
||||
import CryptoWorker from 'utils/crypto';
|
||||
|
@ -37,10 +37,16 @@ export function downloadAsFile(filename: string, content: string) {
|
|||
a.remove();
|
||||
}
|
||||
|
||||
export async function downloadFile(file) {
|
||||
export async function downloadFile(file: File) {
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = await DownloadManger.getFile(file);
|
||||
const cachedFileUrl = await DownloadManager.getCachedOriginalFile(file);
|
||||
const fileURL =
|
||||
cachedFileUrl ??
|
||||
URL.createObjectURL(
|
||||
await new Response(await DownloadManager.downloadFile(file)).blob()
|
||||
);
|
||||
a.href = fileURL;
|
||||
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||
a.download = fileNameWithoutExtension(file.metadata.title) + '.zip';
|
||||
} else {
|
||||
|
@ -51,10 +57,11 @@ export async function downloadFile(file) {
|
|||
a.remove();
|
||||
}
|
||||
|
||||
export function fileIsHEIC(mimeType: string) {
|
||||
export function isFileHEIC(mimeType: string) {
|
||||
return (
|
||||
mimeType.toLowerCase().endsWith(TYPE_HEIC) ||
|
||||
mimeType.toLowerCase().endsWith(TYPE_HEIF)
|
||||
mimeType &&
|
||||
(mimeType.toLowerCase().endsWith(TYPE_HEIC) ||
|
||||
mimeType.toLowerCase().endsWith(TYPE_HEIF))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -271,7 +278,7 @@ export async function convertForPreview(file: File, fileBlob: Blob) {
|
|||
|
||||
const mimeType =
|
||||
(await getMimeTypeFromBlob(worker, fileBlob)) ?? typeFromExtension;
|
||||
if (fileIsHEIC(mimeType)) {
|
||||
if (isFileHEIC(mimeType)) {
|
||||
fileBlob = await worker.convertHEIC2JPEG(fileBlob);
|
||||
}
|
||||
return fileBlob;
|
||||
|
@ -466,3 +473,26 @@ export function getNonTrashedUniqueUserFiles(files: File[]) {
|
|||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function downloadFiles(files: File[]) {
|
||||
for (const file of files) {
|
||||
try {
|
||||
await downloadFile(file);
|
||||
} catch (e) {
|
||||
logError(e, 'download fail for file');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function needsConversionForPreview(file: File) {
|
||||
const fileExtension = splitFilenameAndExtension(file.metadata.title)[1];
|
||||
if (
|
||||
file.metadata.fileType === FILE_TYPE.LIVE_PHOTO ||
|
||||
(file.metadata.fileType === FILE_TYPE.IMAGE &&
|
||||
isFileHEIC(fileExtension))
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue