diff --git a/src/components/PhotoFrame.tsx b/src/components/PhotoFrame.tsx
index d75947e9f..b91ea03ca 100644
--- a/src/components/PhotoFrame.tsx
+++ b/src/components/PhotoFrame.tsx
@@ -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);
diff --git a/src/components/icons/DownloadIcon.tsx b/src/components/icons/DownloadIcon.tsx
new file mode 100644
index 000000000..0172c6dc2
--- /dev/null
+++ b/src/components/icons/DownloadIcon.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+
+export default function DownloadIcon(props) {
+ return (
+
+ );
+}
+
+DownloadIcon.defaultProps = {
+ height: 24,
+ width: 24,
+ viewBox: '0 0 24 24',
+};
diff --git a/src/components/pages/gallery/PreviewCard.tsx b/src/components/pages/gallery/PreviewCard.tsx
index b04963826..ddb4f7de0 100644
--- a/src/components/pages/gallery/PreviewCard.tsx
+++ b/src/components/pages/gallery/PreviewCard.tsx
@@ -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);
diff --git a/src/components/pages/gallery/SelectedFileOptions.tsx b/src/components/pages/gallery/SelectedFileOptions.tsx
index 5581fe7e5..7985e5a1c 100644
--- a/src/components/pages/gallery/SelectedFileOptions.tsx
+++ b/src/components/pages/gallery/SelectedFileOptions.tsx
@@ -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 = ({
)}
+
+
+
+
+
diff --git a/src/pages/gallery/index.tsx b/src/pages/gallery/index.tsx
index 1b2372cb0..3f3976357 100644
--- a/src/pages/gallery/index.tsx
+++ b/src/pages/gallery/index.tsx
@@ -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 (
>();
private thumbnailObjectUrlPromise = new Map>();
- public async getPreview(file: File) {
+ public async getThumbnail(file: File) {
try {
const token = getToken();
if (!token) {
return null;
}
- const thumbnailCache = await caches.open('thumbs');
- const cacheResp: Response = await thumbnailCache.match(
- file.id.toString()
- );
- if (cacheResp) {
- return URL.createObjectURL(await cacheResp.blob());
- }
if (!this.thumbnailObjectUrlPromise.get(file.id)) {
- const downloadPromise = this.downloadThumb(
- token,
- thumbnailCache,
- file
- );
- this.thumbnailObjectUrlPromise.set(file.id, downloadPromise);
+ const downloadPromise = async () => {
+ const thumbnailCache = await caches.open('thumbs');
+ const cacheResp: Response = await thumbnailCache.match(
+ file.id.toString()
+ );
+ if (cacheResp) {
+ return URL.createObjectURL(await cacheResp.blob());
+ }
+ const thumb = await this.downloadThumb(token, file);
+ const thumbBlob = new Blob([thumb]);
+ try {
+ await thumbnailCache.put(
+ file.id.toString(),
+ new Response(thumbBlob)
+ );
+ } 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();
diff --git a/src/services/migrateThumbnailService.ts b/src/services/migrateThumbnailService.ts
index d11286ec5..4245a4538 100644
--- a/src/services/migrateThumbnailService.ts
+++ b/src/services/migrateThumbnailService.ts
@@ -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
);
diff --git a/src/services/upload/thumbnailService.ts b/src/services/upload/thumbnailService.ts
index dd79222f3..8872150df 100644
--- a/src/services/upload/thumbnailService.ts
+++ b/src/services/upload/thumbnailService.ts
@@ -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 {
diff --git a/src/utils/file/index.ts b/src/utils/file/index.ts
index 1e3c66953..1e7b08f25 100644
--- a/src/utils/file/index.ts
+++ b/src/utils/file/index.ts
@@ -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;
+ }
+}