commit
dcef6cda39
|
@ -354,7 +354,7 @@ const PhotoFrame = ({
|
||||||
if (galleryContext.thumbs.has(item.id)) {
|
if (galleryContext.thumbs.has(item.id)) {
|
||||||
url = galleryContext.thumbs.get(item.id);
|
url = galleryContext.thumbs.get(item.id);
|
||||||
} else {
|
} else {
|
||||||
url = await DownloadManager.getPreview(item);
|
url = await DownloadManager.getThumbnail(item);
|
||||||
galleryContext.thumbs.set(item.id, url);
|
galleryContext.thumbs.set(item.id, url);
|
||||||
}
|
}
|
||||||
updateUrl(item.dataIndex)(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) {
|
if (file && !file.msrc) {
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
try {
|
try {
|
||||||
const url = await DownloadManager.getPreview(file);
|
const url = await DownloadManager.getThumbnail(file);
|
||||||
if (isMounted.current) {
|
if (isMounted.current) {
|
||||||
setImgSrc(url);
|
setImgSrc(url);
|
||||||
thumbs.set(file.id, url);
|
thumbs.set(file.id, url);
|
||||||
|
|
|
@ -23,6 +23,7 @@ import {
|
||||||
FIX_CREATION_TIME_VISIBLE_TO_USER_IDS,
|
FIX_CREATION_TIME_VISIBLE_TO_USER_IDS,
|
||||||
User,
|
User,
|
||||||
} from 'services/userService';
|
} from 'services/userService';
|
||||||
|
import DownloadIcon from 'components/icons/DownloadIcon';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
addToCollectionHelper: (collection: Collection) => void;
|
addToCollectionHelper: (collection: Collection) => void;
|
||||||
|
@ -34,6 +35,7 @@ interface Props {
|
||||||
deleteFileHelper: (permanent?: boolean) => void;
|
deleteFileHelper: (permanent?: boolean) => void;
|
||||||
removeFromCollectionHelper: () => void;
|
removeFromCollectionHelper: () => void;
|
||||||
fixTimeHelper: () => void;
|
fixTimeHelper: () => void;
|
||||||
|
downloadHelper: () => void;
|
||||||
count: number;
|
count: number;
|
||||||
clearSelection: () => void;
|
clearSelection: () => void;
|
||||||
archiveFilesHelper: () => void;
|
archiveFilesHelper: () => void;
|
||||||
|
@ -79,6 +81,7 @@ const SelectedFileOptions = ({
|
||||||
setDialogMessage,
|
setDialogMessage,
|
||||||
setCollectionSelectorAttributes,
|
setCollectionSelectorAttributes,
|
||||||
deleteFileHelper,
|
deleteFileHelper,
|
||||||
|
downloadHelper,
|
||||||
count,
|
count,
|
||||||
clearSelection,
|
clearSelection,
|
||||||
archiveFilesHelper,
|
archiveFilesHelper,
|
||||||
|
@ -190,6 +193,11 @@ const SelectedFileOptions = ({
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</IconWithMessage>
|
</IconWithMessage>
|
||||||
)}
|
)}
|
||||||
|
<IconWithMessage message={constants.DOWNLOAD}>
|
||||||
|
<IconButton onClick={downloadHelper}>
|
||||||
|
<DownloadIcon />
|
||||||
|
</IconButton>
|
||||||
|
</IconWithMessage>
|
||||||
<IconWithMessage message={constants.ADD}>
|
<IconWithMessage message={constants.ADD}>
|
||||||
<IconButton onClick={addToCollection}>
|
<IconButton onClick={addToCollection}>
|
||||||
<AddIcon />
|
<AddIcon />
|
||||||
|
|
|
@ -50,6 +50,7 @@ import { LoadingOverlay } from 'components/LoadingOverlay';
|
||||||
import PhotoFrame from 'components/PhotoFrame';
|
import PhotoFrame from 'components/PhotoFrame';
|
||||||
import {
|
import {
|
||||||
changeFilesVisibility,
|
changeFilesVisibility,
|
||||||
|
downloadFiles,
|
||||||
getNonTrashedUniqueUserFiles,
|
getNonTrashedUniqueUserFiles,
|
||||||
getSelectedFiles,
|
getSelectedFiles,
|
||||||
mergeMetadata,
|
mergeMetadata,
|
||||||
|
@ -545,6 +546,14 @@ export default function Gallery() {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const downloadHelper = async () => {
|
||||||
|
const selectedFiles = getSelectedFiles(selected, files);
|
||||||
|
clearSelection();
|
||||||
|
!syncInProgress.current && loadingBar.current?.continuousStart();
|
||||||
|
await downloadFiles(selectedFiles);
|
||||||
|
!syncInProgress.current && loadingBar.current.complete();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GalleryContext.Provider
|
<GalleryContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
@ -714,6 +723,7 @@ export default function Gallery() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
fixTimeHelper={fixTimeHelper}
|
fixTimeHelper={fixTimeHelper}
|
||||||
|
downloadHelper={downloadHelper}
|
||||||
count={selected.count}
|
count={selected.count}
|
||||||
clearSelection={clearSelection}
|
clearSelection={clearSelection}
|
||||||
activeCollection={activeCollection}
|
activeCollection={activeCollection}
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import { getToken } from 'utils/common/key';
|
import { getToken } from 'utils/common/key';
|
||||||
import { getFileUrl, getThumbnailUrl } from 'utils/common/apiUtil';
|
import { getFileUrl, getThumbnailUrl } from 'utils/common/apiUtil';
|
||||||
import CryptoWorker from 'utils/crypto';
|
import CryptoWorker from 'utils/crypto';
|
||||||
import { generateStreamFromArrayBuffer, convertForPreview } from 'utils/file';
|
import {
|
||||||
|
generateStreamFromArrayBuffer,
|
||||||
|
convertForPreview,
|
||||||
|
needsConversionForPreview,
|
||||||
|
} from 'utils/file';
|
||||||
import HTTPService from './HTTPService';
|
import HTTPService from './HTTPService';
|
||||||
import { File, FILE_TYPE } from './fileService';
|
import { File, FILE_TYPE } from './fileService';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
|
@ -10,27 +14,36 @@ class DownloadManager {
|
||||||
private fileObjectUrlPromise = new Map<string, Promise<string>>();
|
private fileObjectUrlPromise = new Map<string, Promise<string>>();
|
||||||
private thumbnailObjectUrlPromise = new Map<number, Promise<string>>();
|
private thumbnailObjectUrlPromise = new Map<number, Promise<string>>();
|
||||||
|
|
||||||
public async getPreview(file: File) {
|
public async getThumbnail(file: File) {
|
||||||
try {
|
try {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return null;
|
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)) {
|
if (!this.thumbnailObjectUrlPromise.get(file.id)) {
|
||||||
const downloadPromise = this.downloadThumb(
|
const downloadPromise = async () => {
|
||||||
token,
|
const thumbnailCache = await caches.open('thumbs');
|
||||||
thumbnailCache,
|
const cacheResp: Response = await thumbnailCache.match(
|
||||||
file
|
file.id.toString()
|
||||||
);
|
);
|
||||||
this.thumbnailObjectUrlPromise.set(file.id, downloadPromise);
|
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);
|
return await this.thumbnailObjectUrlPromise.get(file.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.thumbnailObjectUrlPromise.delete(file.id);
|
this.thumbnailObjectUrlPromise.delete(file.id);
|
||||||
|
@ -39,24 +52,7 @@ class DownloadManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private downloadThumb = async (
|
downloadThumb = async (token: string, file: File) => {
|
||||||
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) => {
|
|
||||||
const resp = await HTTPService.get(
|
const resp = await HTTPService.get(
|
||||||
getThumbnailUrl(file.id),
|
getThumbnailUrl(file.id),
|
||||||
null,
|
null,
|
||||||
|
@ -73,32 +69,38 @@ class DownloadManager {
|
||||||
};
|
};
|
||||||
|
|
||||||
getFile = async (file: File, forPreview = false) => {
|
getFile = async (file: File, forPreview = false) => {
|
||||||
let fileUID: string;
|
const shouldBeConverted = forPreview && needsConversionForPreview(file);
|
||||||
if (file.metadata.fileType === FILE_TYPE.VIDEO) {
|
const fileKey = shouldBeConverted
|
||||||
fileUID = file.id.toString();
|
? `${file.id}_converted`
|
||||||
} else {
|
: `${file.id}`;
|
||||||
fileUID = `${file.id}_forPreview=${forPreview}`;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const getFilePromise = async () => {
|
const getFilePromise = async (convert: boolean) => {
|
||||||
const fileStream = await this.downloadFile(file);
|
const fileStream = await this.downloadFile(file);
|
||||||
let fileBlob = await new Response(fileStream).blob();
|
let fileBlob = await new Response(fileStream).blob();
|
||||||
if (forPreview) {
|
if (convert) {
|
||||||
fileBlob = await convertForPreview(file, fileBlob);
|
fileBlob = await convertForPreview(file, fileBlob);
|
||||||
}
|
}
|
||||||
return URL.createObjectURL(fileBlob);
|
return URL.createObjectURL(fileBlob);
|
||||||
};
|
};
|
||||||
if (!this.fileObjectUrlPromise.get(fileUID)) {
|
if (!this.fileObjectUrlPromise.get(fileKey)) {
|
||||||
this.fileObjectUrlPromise.set(fileUID, getFilePromise());
|
this.fileObjectUrlPromise.set(
|
||||||
|
fileKey,
|
||||||
|
getFilePromise(shouldBeConverted)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return await this.fileObjectUrlPromise.get(fileUID);
|
const fileURL = await this.fileObjectUrlPromise.get(fileKey);
|
||||||
|
return fileURL;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.fileObjectUrlPromise.delete(fileUID);
|
this.fileObjectUrlPromise.delete(fileKey);
|
||||||
logError(e, 'Failed to get File');
|
logError(e, 'Failed to get File');
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public async getCachedOriginalFile(file: File) {
|
||||||
|
return await this.fileObjectUrlPromise.get(file.id.toString());
|
||||||
|
}
|
||||||
|
|
||||||
async downloadFile(file: File) {
|
async downloadFile(file: File) {
|
||||||
const worker = await new CryptoWorker();
|
const worker = await new CryptoWorker();
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
|
|
|
@ -67,7 +67,7 @@ export async function replaceThumbnail(
|
||||||
current: idx,
|
current: idx,
|
||||||
total: largeThumbnailFiles.length,
|
total: largeThumbnailFiles.length,
|
||||||
});
|
});
|
||||||
const originalThumbnail = await downloadManager.getThumbnail(
|
const originalThumbnail = await downloadManager.downloadThumb(
|
||||||
token,
|
token,
|
||||||
file
|
file
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { logError } from 'utils/sentry';
|
||||||
import { BLACK_THUMBNAIL_BASE64 } from '../../../public/images/black-thumbnail-b64';
|
import { BLACK_THUMBNAIL_BASE64 } from '../../../public/images/black-thumbnail-b64';
|
||||||
import FFmpegService from 'services/ffmpegService';
|
import FFmpegService from 'services/ffmpegService';
|
||||||
import { convertToHumanReadable } from 'utils/billingUtil';
|
import { convertToHumanReadable } from 'utils/billingUtil';
|
||||||
import { fileIsHEIC } from 'utils/file';
|
import { isFileHEIC } from 'utils/file';
|
||||||
import { FileTypeInfo } from './readFileService';
|
import { FileTypeInfo } from './readFileService';
|
||||||
|
|
||||||
const MAX_THUMBNAIL_DIMENSION = 720;
|
const MAX_THUMBNAIL_DIMENSION = 720;
|
||||||
|
@ -31,7 +31,7 @@ export async function generateThumbnail(
|
||||||
let thumbnail: Uint8Array;
|
let thumbnail: Uint8Array;
|
||||||
try {
|
try {
|
||||||
if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) {
|
if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) {
|
||||||
const isHEIC = fileIsHEIC(fileTypeInfo.exactType);
|
const isHEIC = isFileHEIC(fileTypeInfo.exactType);
|
||||||
canvas = await generateImageThumbnail(worker, file, isHEIC);
|
canvas = await generateImageThumbnail(worker, file, isHEIC);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
} from 'services/fileService';
|
} from 'services/fileService';
|
||||||
import { decodeMotionPhoto } from 'services/motionPhotoService';
|
import { decodeMotionPhoto } from 'services/motionPhotoService';
|
||||||
import { getMimeTypeFromBlob } from 'services/upload/readFileService';
|
import { getMimeTypeFromBlob } from 'services/upload/readFileService';
|
||||||
import DownloadManger from 'services/downloadManager';
|
import DownloadManager from 'services/downloadManager';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import { User } from 'services/userService';
|
import { User } from 'services/userService';
|
||||||
import CryptoWorker from 'utils/crypto';
|
import CryptoWorker from 'utils/crypto';
|
||||||
|
@ -37,10 +37,16 @@ export function downloadAsFile(filename: string, content: string) {
|
||||||
a.remove();
|
a.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function downloadFile(file) {
|
export async function downloadFile(file: File) {
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.style.display = 'none';
|
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) {
|
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||||
a.download = fileNameWithoutExtension(file.metadata.title) + '.zip';
|
a.download = fileNameWithoutExtension(file.metadata.title) + '.zip';
|
||||||
} else {
|
} else {
|
||||||
|
@ -51,10 +57,11 @@ export async function downloadFile(file) {
|
||||||
a.remove();
|
a.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fileIsHEIC(mimeType: string) {
|
export function isFileHEIC(mimeType: string) {
|
||||||
return (
|
return (
|
||||||
mimeType.toLowerCase().endsWith(TYPE_HEIC) ||
|
mimeType &&
|
||||||
mimeType.toLowerCase().endsWith(TYPE_HEIF)
|
(mimeType.toLowerCase().endsWith(TYPE_HEIC) ||
|
||||||
|
mimeType.toLowerCase().endsWith(TYPE_HEIF))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,7 +278,7 @@ export async function convertForPreview(file: File, fileBlob: Blob) {
|
||||||
|
|
||||||
const mimeType =
|
const mimeType =
|
||||||
(await getMimeTypeFromBlob(worker, fileBlob)) ?? typeFromExtension;
|
(await getMimeTypeFromBlob(worker, fileBlob)) ?? typeFromExtension;
|
||||||
if (fileIsHEIC(mimeType)) {
|
if (isFileHEIC(mimeType)) {
|
||||||
fileBlob = await worker.convertHEIC2JPEG(fileBlob);
|
fileBlob = await worker.convertHEIC2JPEG(fileBlob);
|
||||||
}
|
}
|
||||||
return 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