diff --git a/configUtil.js b/configUtil.js
index 4bab3d5f0..274c3d786 100644
--- a/configUtil.js
+++ b/configUtil.js
@@ -17,8 +17,10 @@ module.exports = {
},
CSP_DIRECTIVES: {
- 'default-src': "'none'",
- 'img-src': "'self' blob:",
+ // self is safe enough
+ 'default-src': "'self'",
+ // data to allow two factor qr code
+ 'img-src': "'self' blob: data:",
'media-src': "'self' blob:",
'manifest-src': "'self'",
'style-src': "'self' 'unsafe-inline'",
@@ -26,10 +28,13 @@ module.exports = {
'connect-src':
"'self' https://*.ente.io http://localhost:8080 data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com ",
'base-uri ': "'self'",
+ // to allow worker
+ 'child-src': "'self' blob:",
+ 'object-src': "'none'",
'frame-ancestors': " 'none'",
'form-action': "'none'",
- 'report-uri': ' https://csp-reporter.ente.io',
- 'report-to': ' https://csp-reporter.ente.io',
+ 'report-uri': ' https://csp-reporter.ente.io/local',
+ 'report-to': ' https://csp-reporter.ente.io/local',
},
WORKBOX_CONFIG: {
diff --git a/package.json b/package.json
index 5a32eef78..746cf4ecc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "bada-frame",
- "version": "0.7.0",
+ "version": "0.8.0",
"private": true,
"scripts": {
"dev": "next dev",
diff --git a/public/_headers b/public/_headers
index 18af99761..388d4038c 100644
--- a/public/_headers
+++ b/public/_headers
@@ -8,5 +8,5 @@
X-Frame-Options: deny
X-XSS-Protection: 1; mode=block
Referrer-Policy: same-origin
- Content-Security-Policy-Report-Only: default-src 'none'; img-src 'self' blob:; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' 'unsafe-eval' blob:; manifest-src 'self'; connect-src 'self' https://*.ente.io data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com ; base-uri 'self'; frame-ancestors 'none'; form-action 'none'; report-uri https://csp-reporter.ente.io; report-to https://csp-reporter.ente.io;
+ Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' blob: data:; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' 'unsafe-eval' blob:; manifest-src 'self'; child-src 'self' blob:; object-src 'none'; connect-src 'self' https://*.ente.io data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com ; base-uri 'self'; frame-ancestors 'none'; form-action 'none'; report-uri https://csp-reporter.ente.io; report-to https://csp-reporter.ente.io;
diff --git a/src/components/LivePhotoBtn.tsx b/src/components/LivePhotoBtn.tsx
new file mode 100644
index 000000000..bb1976db9
--- /dev/null
+++ b/src/components/LivePhotoBtn.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+
+export const livePhotoBtnHTML = (
+
+);
diff --git a/src/components/PhotoFrame.tsx b/src/components/PhotoFrame.tsx
index e54d4938b..160b074cc 100644
--- a/src/components/PhotoFrame.tsx
+++ b/src/components/PhotoFrame.tsx
@@ -72,6 +72,11 @@ interface Props {
enableDownload: boolean;
}
+type SourceURL = {
+ imageURL?: string;
+ videoURL?: string;
+};
+
const PhotoFrame = ({
files,
setFiles,
@@ -103,6 +108,7 @@ const PhotoFrame = ({
const filteredDataRef = useRef([]);
const filteredData = filteredDataRef?.current ?? [];
const router = useRouter();
+ const [isSourceLoaded, setIsSourceLoaded] = useState(false);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Shift') {
@@ -244,18 +250,15 @@ const PhotoFrame = ({
}, [open]);
const updateURL = (index: number) => (url: string) => {
- files[index] = {
- ...files[index],
- msrc: url,
- src: files[index].src ? files[index].src : url,
- w: window.innerWidth,
- h: window.innerHeight,
- };
- if (
- files[index].metadata.fileType === FILE_TYPE.VIDEO &&
- !files[index].html
- ) {
- files[index].html = `
+ const updateFile = (file: EnteFile) => {
+ file = {
+ ...file,
+ msrc: url,
+ w: window.innerWidth,
+ h: window.innerHeight,
+ };
+ if (file.metadata.fileType === FILE_TYPE.VIDEO && !file.html) {
+ file.html = `
@@ -263,46 +266,94 @@ const PhotoFrame = ({
`;
- delete files[index].src;
- }
- if (
- files[index].metadata.fileType === FILE_TYPE.IMAGE &&
- !files[index].src
- ) {
- files[index].src = url;
- }
- setFiles(files);
+ } else if (
+ file.metadata.fileType === FILE_TYPE.LIVE_PHOTO &&
+ !file.html
+ ) {
+ file.html = `
+
+
+
+ Loading...
+
+
+ `;
+ } else if (
+ file.metadata.fileType === FILE_TYPE.IMAGE &&
+ !file.src
+ ) {
+ file.src = url;
+ }
+ return file;
+ };
+ setFiles((files) => {
+ files[index] = updateFile(files[index]);
+ return [...files];
+ });
+ return updateFile(files[index]);
};
- const updateSrcURL = async (index: number, url: string) => {
- files[index] = {
- ...files[index],
- w: window.innerWidth,
- h: window.innerHeight,
- };
- if (files[index].metadata.fileType === FILE_TYPE.VIDEO) {
- if (await isPlaybackPossible(url)) {
- files[index].html = `
-
+ const updateSrcURL = async (index: number, srcURL: SourceURL) => {
+ const { videoURL, imageURL } = srcURL;
+ const isPlayable = videoURL && (await isPlaybackPossible(videoURL));
+ const updateFile = (file: EnteFile) => {
+ file = {
+ ...file,
+ w: window.innerWidth,
+ h: window.innerHeight,
+ };
+ if (file.metadata.fileType === FILE_TYPE.VIDEO) {
+ if (isPlayable) {
+ file.html = `
+
+ `;
+ } else {
+ file.html = `
+
+
+
+ ${constants.VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD}
+
Download
+
+
`;
- } else {
- files[index].html = `
+ }
+ } else if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
+ if (isPlayable) {
+ file.html = `
+
+
+
+
+ `;
+ } else {
+ file.html = `
-
-
+
+
`;
+ }
+ } else {
+ file.src = imageURL;
}
- } else {
- files[index].src = url;
- }
- setFiles(files);
+ return file;
+ };
+ setFiles((files) => {
+ files[index] = updateFile(files[index]);
+ return [...files];
+ });
+ setIsSourceLoaded(true);
+ return updateFile(files[index]);
};
const handleClose = (needUpdate) => {
@@ -418,13 +469,13 @@ const PhotoFrame = ({
}
galleryContext.thumbs.set(item.id, url);
}
- updateURL(item.dataIndex)(url);
- item.msrc = url;
- if (!item.src) {
- item.src = url;
- }
- item.w = window.innerWidth;
- item.h = window.innerHeight;
+ const newFile = updateURL(item.dataIndex)(url);
+ item.msrc = newFile.msrc;
+ item.html = newFile.html;
+ item.src = newFile.src;
+ item.w = newFile.w;
+ item.h = newFile.h;
+
try {
instance.invalidateCurrItems();
instance.updateSize(true);
@@ -438,29 +489,47 @@ const PhotoFrame = ({
if (!fetching[item.dataIndex]) {
try {
fetching[item.dataIndex] = true;
- let url: string;
+ let urls: string[];
if (galleryContext.files.has(item.id)) {
- url = galleryContext.files.get(item.id);
+ const mergedURL = galleryContext.files.get(item.id);
+ urls = mergedURL.split(',');
} else {
+ galleryContext.startLoading();
if (
publicCollectionGalleryContext.accessedThroughSharedURL
) {
- url = await PublicCollectionDownloadManager.getFile(
+ urls = await PublicCollectionDownloadManager.getFile(
item,
publicCollectionGalleryContext.token,
publicCollectionGalleryContext.passwordToken,
true
);
} else {
- url = await DownloadManager.getFile(item, true);
+ urls = await DownloadManager.getFile(item, true);
}
- galleryContext.files.set(item.id, url);
+ galleryContext.finishLoading();
+ const mergedURL = urls.join(',');
+ galleryContext.files.set(item.id, mergedURL);
}
- await updateSrcURL(item.dataIndex, url);
- item.html = files[item.dataIndex].html;
- item.src = files[item.dataIndex].src;
- item.w = files[item.dataIndex].w;
- item.h = files[item.dataIndex].h;
+ let imageURL;
+ let videoURL;
+ if (item.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
+ [imageURL, videoURL] = urls;
+ } else if (item.metadata.fileType === FILE_TYPE.VIDEO) {
+ [videoURL] = urls;
+ } else {
+ [imageURL] = urls;
+ }
+ setIsSourceLoaded(false);
+ const newFile = await updateSrcURL(item.dataIndex, {
+ imageURL,
+ videoURL,
+ });
+ item.msrc = newFile.msrc;
+ item.html = newFile.html;
+ item.src = newFile.src;
+ item.w = newFile.w;
+ item.h = newFile.h;
try {
instance.invalidateCurrItems();
instance.updateSize(true);
@@ -524,6 +593,7 @@ const PhotoFrame = ({
isSharedCollection={isSharedCollection}
isTrashCollection={activeCollection === TRASH_SECTION}
enableDownload={enableDownload}
+ isSourceLoaded={isSourceLoaded}
/>
)}
diff --git a/src/components/PhotoSwipe/PhotoSwipe.tsx b/src/components/PhotoSwipe/PhotoSwipe.tsx
index 8757378a3..f67ce1c6b 100644
--- a/src/components/PhotoSwipe/PhotoSwipe.tsx
+++ b/src/components/PhotoSwipe/PhotoSwipe.tsx
@@ -20,7 +20,6 @@ import {
changeFileName,
downloadFile,
formatDateTime,
- isLivePhoto,
splitFilenameAndExtension,
updateExistingFilePubMetadata,
} from 'utils/file';
@@ -35,6 +34,7 @@ import {
Row,
Value,
} from 'components/Container';
+import { livePhotoBtnHTML } from 'components/LivePhotoBtn';
import { logError } from 'utils/sentry';
import CloseIcon from 'components/icons/CloseIcon';
@@ -43,14 +43,11 @@ import { Formik } from 'formik';
import * as Yup from 'yup';
import EnteSpinner from 'components/EnteSpinner';
import EnteDateTimePicker from 'components/EnteDateTimePicker';
-import { MAX_EDITED_FILE_NAME_LENGTH } from 'constants/file';
+import { MAX_EDITED_FILE_NAME_LENGTH, FILE_TYPE } from 'constants/file';
import { sleep } from 'utils/common';
+import { playVideo, pauseVideo } from 'utils/photoFrame';
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
import { GalleryContext } from 'pages/gallery';
-import {
- getLivePhotoInfoShownCount,
- setLivePhotoInfoShownCount,
-} from 'utils/storage';
const SmallLoadingSpinner = () => (
(props.disabled ? 'not-allowed' : 'pointer')};
+ }
+`;
+
+const livePhotoDefaultOptions = {
+ click: () => {},
+ hide: () => {},
+ show: () => {},
+ loading: false,
+ visible: false,
+};
+
const renderInfoItem = (label: string, value: string | JSX.Element) => (
@@ -490,13 +511,17 @@ function InfoModal({
function PhotoSwipe(props: Iprops) {
const pswpElement = useRef();
- const [photoSwipe, setPhotoSwipe] = useState>();
+ const [photoSwipe, setPhotoSwipe] =
+ useState>();
- const { isOpen, items } = props;
+ const { isOpen, items, isSourceLoaded } = props;
const [isFav, setIsFav] = useState(false);
const [showInfo, setShowInfo] = useState(false);
const [metadata, setMetaData] = useState(null);
const [exif, setExif] = useState(null);
+ const [livePhotoBtnOptions, setLivePhotoBtnOptions] = useState(
+ livePhotoDefaultOptions
+ );
const needUpdate = useRef(false);
const publicCollectionGalleryContext = useContext(
PublicCollectionGalleryContext
@@ -527,23 +552,75 @@ function PhotoSwipe(props: Iprops) {
}
}, [showInfo]);
+ useEffect(() => {
+ if (!isOpen) return;
+ const item = items[photoSwipe?.getCurrentIndex()];
+ if (item && item.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
+ const getVideoAndImage = () => {
+ const video = document.getElementById(
+ `live-photo-video-${item.id}`
+ );
+ const image = document.getElementById(
+ `live-photo-image-${item.id}`
+ );
+ return { video, image };
+ };
+
+ const { video, image } = getVideoAndImage();
+
+ if (video && image) {
+ setLivePhotoBtnOptions({
+ click: async () => {
+ await playVideo(video, image);
+ },
+ hide: async () => {
+ await pauseVideo(video, image);
+ },
+ show: async () => {
+ await playVideo(video, image);
+ },
+ visible: true,
+ loading: false,
+ });
+ } else {
+ setLivePhotoBtnOptions({
+ ...livePhotoDefaultOptions,
+ visible: true,
+ loading: true,
+ });
+ }
+
+ const downloadLivePhotoBtn = document.getElementById(
+ `download-btn-${item.id}`
+ ) as HTMLButtonElement;
+ if (downloadLivePhotoBtn) {
+ const downloadLivePhoto = () => {
+ downloadFileHelper(photoSwipe.currItem);
+ };
+
+ downloadLivePhotoBtn.addEventListener(
+ 'click',
+ downloadLivePhoto
+ );
+ return () => {
+ downloadLivePhotoBtn.removeEventListener(
+ 'click',
+ downloadLivePhoto
+ );
+ setLivePhotoBtnOptions(livePhotoDefaultOptions);
+ };
+ }
+
+ return () => {
+ setLivePhotoBtnOptions(livePhotoDefaultOptions);
+ };
+ }
+ }, [photoSwipe?.currItem, isOpen, isSourceLoaded]);
+
function updateFavButton() {
setIsFav(isInFav(this?.currItem));
}
- function handleLivePhotoNotification() {
- if (isLivePhoto(this?.currItem)) {
- const infoShownCount = getLivePhotoInfoShownCount();
- if (infoShownCount < 3) {
- galleryContext.setNotificationAttributes({
- message: constants.PLAYBACK_SUPPORT_COMING,
- title: constants.LIVE_PHOTO,
- });
- setLivePhotoInfoShownCount(infoShownCount + 1);
- }
- }
- }
-
const openPhotoSwipe = () => {
const { items, currentIndex } = props;
const options = {
@@ -606,7 +683,6 @@ function PhotoSwipe(props: Iprops) {
photoSwipe.listen('beforeChange', function () {
updateInfo.call(this);
updateFavButton.call(this);
- handleLivePhotoNotification.call(this);
});
photoSwipe.listen('resize', checkExifAvailable);
photoSwipe.init();
@@ -726,6 +802,18 @@ function PhotoSwipe(props: Iprops) {
ref={pswpElement}>
+
+ {livePhotoBtnHTML} {constants.LIVE}
+
diff --git a/src/components/pages/gallery/PlanSelector.tsx b/src/components/pages/gallery/PlanSelector.tsx
index 15533acfb..a5ad2c342 100644
--- a/src/components/pages/gallery/PlanSelector.tsx
+++ b/src/components/pages/gallery/PlanSelector.tsx
@@ -16,6 +16,8 @@ import {
hasPaidSubscription,
isOnFreePlan,
planForSubscription,
+ hasMobileSubscription,
+ hasPaypalSubscription,
} from 'utils/billing';
import { reverseString } from 'utils/common';
import { SetDialogMessage } from 'components/MessageDialog';
@@ -143,8 +145,7 @@ function PlanSelector(props: Props) {
async function onPlanSelect(plan: Plan) {
if (
- hasPaidSubscription(subscription) &&
- !hasStripeSubscription(subscription) &&
+ hasMobileSubscription(subscription) &&
!isSubscriptionCancelled(subscription)
) {
props.setDialogMessage({
@@ -152,6 +153,15 @@ function PlanSelector(props: Props) {
content: constants.CANCEL_SUBSCRIPTION_ON_MOBILE,
close: { variant: 'danger' },
});
+ } else if (
+ hasPaypalSubscription(subscription) &&
+ !isSubscriptionCancelled(subscription)
+ ) {
+ props.setDialogMessage({
+ title: constants.MANAGE_PLAN,
+ content: constants.PAYPAL_MANAGE_NOT_SUPPORTED_MESSAGE(),
+ close: { variant: 'danger' },
+ });
} else if (hasStripeSubscription(subscription)) {
props.setDialogMessage({
title: `${constants.CONFIRM} ${reverseString(
diff --git a/src/components/pages/gallery/PreviewCard.tsx b/src/components/pages/gallery/PreviewCard.tsx
index 1a4810528..51a100226 100644
--- a/src/components/pages/gallery/PreviewCard.tsx
+++ b/src/components/pages/gallery/PreviewCard.tsx
@@ -164,7 +164,7 @@ const Cont = styled.div<{ disabled: boolean; selected: boolean }>`
export default function PreviewCard(props: IProps) {
const [imgSrc, setImgSrc] = useState
();
- const { thumbs, files } = useContext(GalleryContext);
+ const { thumbs } = useContext(GalleryContext);
const {
file,
onClick,
@@ -203,10 +203,6 @@ export default function PreviewCard(props: IProps) {
if (isMounted.current) {
setImgSrc(url);
thumbs.set(file.id, url);
- file.msrc = url;
- if (!file.src) {
- file.src = url;
- }
updateURL(url);
}
} catch (e) {
@@ -218,13 +214,6 @@ export default function PreviewCard(props: IProps) {
const thumbImgSrc = thumbs.get(file.id);
setImgSrc(thumbImgSrc);
file.msrc = thumbImgSrc;
- if (!file.src) {
- if (files.has(file.id)) {
- file.src = files.get(file.id);
- } else {
- file.src = thumbImgSrc;
- }
- }
} else {
main();
}
diff --git a/src/constants/upload/index.ts b/src/constants/upload/index.ts
index 2109e566b..fb9f0a15d 100644
--- a/src/constants/upload/index.ts
+++ b/src/constants/upload/index.ts
@@ -1,6 +1,6 @@
import { ENCRYPTION_CHUNK_SIZE } from 'constants/crypto';
import { FILE_TYPE } from 'constants/file';
-import { Location } from 'types/upload';
+import { Location, ParsedExtractedMetadata } from 'types/upload';
// list of format that were missed by type-detection for some files.
export const FORMAT_MISSED_BY_FILE_TYPE_LIB = [
@@ -43,3 +43,8 @@ export enum FileUploadResults {
export const MAX_FILE_SIZE_SUPPORTED = 5 * 1024 * 1024 * 1024; // 5 GB
export const LIVE_PHOTO_ASSET_SIZE_LIMIT = 20 * 1024 * 1024; // 20MB
+
+export const NULL_EXTRACTED_METADATA: ParsedExtractedMetadata = {
+ location: NULL_LOCATION,
+ creationTime: null,
+};
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 031791c39..a9c8e7eae 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -69,6 +69,35 @@ const GlobalStyles = createGlobalStyle`
height: 100%;
}
+ .live-photo-container{
+ width: 100%;
+ height: 100%;
+ position: relative;
+ object-fit: contain;
+ }
+
+ .live-photo-container > img{
+ opacity: 1;
+ max-width: 100%;
+ max-height: 100%;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ transition: opacity 1s ease;
+ }
+
+ .live-photo-container > video{
+ opacity: 0;
+ max-width: 100%;
+ max-height: 100%;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ transition: opacity 1s ease;
+ }
+
.video-loading {
width: 100%;
height: 100%;
diff --git a/src/services/downloadManager.ts b/src/services/downloadManager.ts
index 5752b1262..6f53f79ac 100644
--- a/src/services/downloadManager.ts
+++ b/src/services/downloadManager.ts
@@ -14,7 +14,7 @@ import { FILE_TYPE } from 'constants/file';
import { CustomError } from 'utils/error';
class DownloadManager {
- private fileObjectURLPromise = new Map>();
+ private fileObjectURLPromise = new Map>();
private thumbnailObjectURLPromise = new Map>();
public async getThumbnail(file: EnteFile) {
@@ -90,11 +90,17 @@ class DownloadManager {
try {
const getFilePromise = async (convert: boolean) => {
const fileStream = await this.downloadFile(file);
- let fileBlob = await new Response(fileStream).blob();
+ const fileBlob = await new Response(fileStream).blob();
if (convert) {
- fileBlob = await convertForPreview(file, fileBlob);
+ const convertedBlobs = await convertForPreview(
+ file,
+ fileBlob
+ );
+ return convertedBlobs.map((blob) =>
+ URL.createObjectURL(blob)
+ );
}
- return URL.createObjectURL(fileBlob);
+ return [URL.createObjectURL(fileBlob)];
};
if (!this.fileObjectURLPromise.get(fileKey)) {
this.fileObjectURLPromise.set(
@@ -102,8 +108,8 @@ class DownloadManager {
getFilePromise(shouldBeConverted)
);
}
- const fileURL = await this.fileObjectURLPromise.get(fileKey);
- return fileURL;
+ const fileURLs = await this.fileObjectURLPromise.get(fileKey);
+ return fileURLs;
} catch (e) {
this.fileObjectURLPromise.delete(fileKey);
logError(e, 'Failed to get File');
diff --git a/src/services/ffmpegService.ts b/src/services/ffmpegService.ts
index 3914cfea2..59a03d4b4 100644
--- a/src/services/ffmpegService.ts
+++ b/src/services/ffmpegService.ts
@@ -2,14 +2,17 @@ import { createFFmpeg, FFmpeg } from '@ffmpeg/ffmpeg';
import { CustomError } from 'utils/error';
import { logError } from 'utils/sentry';
import QueueProcessor from './queueProcessor';
+import { ParsedExtractedMetadata } from 'types/upload';
+
import { getUint8ArrayView } from './upload/readFileService';
+import { parseFFmpegExtractedMetadata } from './upload/videoMetadataService';
class FFmpegService {
private ffmpeg: FFmpeg = null;
private isLoading = null;
private fileReader: FileReader = null;
- private generateThumbnailProcessor = new QueueProcessor(1);
+ private ffmpegTaskQueue = new QueueProcessor(1);
async init() {
try {
this.ffmpeg = createFFmpeg({
@@ -26,7 +29,7 @@ class FFmpegService {
}
}
- async generateThumbnail(file: File) {
+ async generateThumbnail(file: File): Promise {
if (!this.ffmpeg) {
await this.init();
}
@@ -36,7 +39,7 @@ class FFmpegService {
if (this.isLoading) {
await this.isLoading;
}
- const response = this.generateThumbnailProcessor.queueUpRequest(
+ const response = this.ffmpegTaskQueue.queueUpRequest(
generateThumbnailHelper.bind(
null,
this.ffmpeg,
@@ -56,6 +59,65 @@ class FFmpegService {
}
}
}
+
+ async extractMetadata(file: File): Promise {
+ if (!this.ffmpeg) {
+ await this.init();
+ }
+ if (!this.fileReader) {
+ this.fileReader = new FileReader();
+ }
+ if (this.isLoading) {
+ await this.isLoading;
+ }
+ const response = this.ffmpegTaskQueue.queueUpRequest(
+ extractVideoMetadataHelper.bind(
+ null,
+ this.ffmpeg,
+ this.fileReader,
+ file
+ )
+ );
+ try {
+ return await response.promise;
+ } catch (e) {
+ if (e.message === CustomError.REQUEST_CANCELLED) {
+ // ignore
+ return null;
+ } else {
+ logError(e, 'ffmpeg metadata extraction failed');
+ throw e;
+ }
+ }
+ }
+
+ async convertToMP4(
+ file: Uint8Array,
+ fileName: string
+ ): Promise {
+ if (!this.ffmpeg) {
+ await this.init();
+ }
+ if (this.isLoading) {
+ await this.isLoading;
+ }
+
+ const response = this.ffmpegTaskQueue.queueUpRequest(
+ convertToMP4Helper.bind(null, this.ffmpeg, file, fileName)
+ );
+
+ try {
+ return await response.promise;
+ } catch (e) {
+ if (e.message === CustomError.REQUEST_CANCELLED) {
+ // ignore
+ return null;
+ } else {
+ logError(e, 'ffmpeg MP4 conversion failed');
+ throw e;
+ }
+ }
+ }
}
async function generateThumbnailHelper(
@@ -101,4 +163,68 @@ async function generateThumbnailHelper(
}
}
+async function extractVideoMetadataHelper(
+ ffmpeg: FFmpeg,
+ reader: FileReader,
+ file: File
+) {
+ try {
+ const inputFileName = `${Date.now().toString()}-${file.name}`;
+ const outFileName = `${Date.now().toString()}-metadata.txt`;
+ ffmpeg.FS(
+ 'writeFile',
+ inputFileName,
+ await getUint8ArrayView(reader, file)
+ );
+ let metadata = null;
+
+ // https://stackoverflow.com/questions/9464617/retrieving-and-saving-media-metadata-using-ffmpeg
+ // -c [short for codex] copy[(stream_specifier)[ffmpeg.org/ffmpeg.html#Stream-specifiers]] => copies all the stream without re-encoding
+ // -map_metadata [http://ffmpeg.org/ffmpeg.html#Advanced-options search for map_metadata] => copies all stream metadata to the out
+ // -f ffmetadata [https://ffmpeg.org/ffmpeg-formats.html#Metadata-1] => dump metadata from media files into a simple UTF-8-encoded INI-like text file
+ await ffmpeg.run(
+ '-i',
+ inputFileName,
+ '-c',
+ 'copy',
+ '-map_metadata',
+ '0',
+ '-f',
+ 'ffmetadata',
+ outFileName
+ );
+ metadata = ffmpeg.FS('readFile', outFileName);
+ ffmpeg.FS('unlink', outFileName);
+ ffmpeg.FS('unlink', inputFileName);
+ return parseFFmpegExtractedMetadata(metadata);
+ } catch (e) {
+ logError(e, 'ffmpeg metadata extraction failed');
+ throw e;
+ }
+}
+
+async function convertToMP4Helper(
+ ffmpeg: FFmpeg,
+ file: Uint8Array,
+ inputFileName: string
+) {
+ try {
+ ffmpeg.FS('writeFile', inputFileName, file);
+ await ffmpeg.run(
+ '-i',
+ inputFileName,
+ '-preset',
+ 'ultrafast',
+ 'output.mp4'
+ );
+ const convertedFile = ffmpeg.FS('readFile', 'output.mp4');
+ ffmpeg.FS('unlink', inputFileName);
+ ffmpeg.FS('unlink', 'output.mp4');
+ return convertedFile;
+ } catch (e) {
+ logError(e, 'ffmpeg MP4 conversion failed');
+ throw e;
+ }
+}
+
export default new FFmpegService();
diff --git a/src/services/fileService.ts b/src/services/fileService.ts
index 8d1ad4944..540f0dbfe 100644
--- a/src/services/fileService.ts
+++ b/src/services/fileService.ts
@@ -6,9 +6,15 @@ import { EncryptionResult } from 'types/upload';
import { Collection } from 'types/collection';
import HTTPService from './HTTPService';
import { logError } from 'utils/sentry';
-import { decryptFile, mergeMetadata, sortFiles } from 'utils/file';
+import {
+ decryptFile,
+ mergeMetadata,
+ preservePhotoswipeProps,
+ sortFiles,
+} from 'utils/file';
import CryptoWorker from 'utils/crypto';
import { EnteFile, TrashRequest, UpdateMagicMetadataRequest } from 'types/file';
+import { SetFiles } from 'types/gallery';
const ENDPOINT = getEndpoint();
const FILES_TABLE = 'files';
@@ -28,13 +34,13 @@ const getCollectionLastSyncTime = async (collection: Collection) =>
export const syncFiles = async (
collections: Collection[],
- setFiles: (files: EnteFile[]) => void
+ setFiles: SetFiles
) => {
const localFiles = await getLocalFiles();
let files = await removeDeletedCollectionFiles(collections, localFiles);
if (files.length !== localFiles.length) {
await setLocalFiles(files);
- setFiles([...sortFiles(mergeMetadata(files))]);
+ setFiles(preservePhotoswipeProps([...sortFiles(mergeMetadata(files))]));
}
for (const collection of collections) {
if (!getToken()) {
@@ -70,7 +76,7 @@ export const syncFiles = async (
`${collection.id}-time`,
collection.updationTime
);
- setFiles([...sortFiles(mergeMetadata(files))]);
+ setFiles(preservePhotoswipeProps([...sortFiles(mergeMetadata(files))]));
}
return sortFiles(mergeMetadata(files));
};
@@ -79,7 +85,7 @@ export const getFiles = async (
collection: Collection,
sinceTime: number,
files: EnteFile[],
- setFiles: (files: EnteFile[]) => void
+ setFiles: SetFiles
): Promise => {
try {
const decryptedFiles: EnteFile[] = [];
@@ -116,10 +122,12 @@ export const getFiles = async (
time = resp.data.diff.slice(-1)[0].updationTime;
}
setFiles(
- sortFiles(
- mergeMetadata(
- [...(files || []), ...decryptedFiles].filter(
- (item) => !item.isDeleted
+ preservePhotoswipeProps(
+ sortFiles(
+ mergeMetadata(
+ [...(files || []), ...decryptedFiles].filter(
+ (item) => !item.isDeleted
+ )
)
)
)
diff --git a/src/services/publicCollectionDownloadManager.ts b/src/services/publicCollectionDownloadManager.ts
index 0326c9e7c..70a4c7b38 100644
--- a/src/services/publicCollectionDownloadManager.ts
+++ b/src/services/publicCollectionDownloadManager.ts
@@ -16,7 +16,7 @@ import { FILE_TYPE } from 'constants/file';
import { CustomError } from 'utils/error';
class PublicCollectionDownloadManager {
- private fileObjectURLPromise = new Map>();
+ private fileObjectURLPromise = new Map>();
private thumbnailObjectURLPromise = new Map>();
public async getThumbnail(
@@ -116,11 +116,17 @@ class PublicCollectionDownloadManager {
passwordToken,
file
);
- let fileBlob = await new Response(fileStream).blob();
+ const fileBlob = await new Response(fileStream).blob();
if (convert) {
- fileBlob = await convertForPreview(file, fileBlob);
+ const convertedBlobs = await convertForPreview(
+ file,
+ fileBlob
+ );
+ return convertedBlobs.map((blob) =>
+ URL.createObjectURL(blob)
+ );
}
- return URL.createObjectURL(fileBlob);
+ return [URL.createObjectURL(fileBlob)];
};
if (!this.fileObjectURLPromise.get(fileKey)) {
this.fileObjectURLPromise.set(
@@ -128,8 +134,8 @@ class PublicCollectionDownloadManager {
getFilePromise(shouldBeConverted)
);
}
- const fileURL = await this.fileObjectURLPromise.get(fileKey);
- return fileURL;
+ const fileURLs = await this.fileObjectURLPromise.get(fileKey);
+ return fileURLs;
} catch (e) {
this.fileObjectURLPromise.delete(fileKey);
logError(e, 'Failed to get File');
diff --git a/src/services/trashService.ts b/src/services/trashService.ts
index c53413074..4fba7c05e 100644
--- a/src/services/trashService.ts
+++ b/src/services/trashService.ts
@@ -2,7 +2,12 @@ import { SetFiles } from 'types/gallery';
import { Collection } from 'types/collection';
import { getEndpoint } from 'utils/common/apiUtil';
import { getToken } from 'utils/common/key';
-import { decryptFile, mergeMetadata, sortFiles } from 'utils/file';
+import {
+ decryptFile,
+ mergeMetadata,
+ preservePhotoswipeProps,
+ sortFiles,
+} from 'utils/file';
import { logError } from 'utils/sentry';
import localForage from 'utils/storage/localForage';
import { getCollection } from './collectionService';
@@ -120,7 +125,12 @@ export const updateTrash = async (
updatedTrash = removeRestoredOrDeletedTrashItems(updatedTrash);
setFiles(
- sortFiles([...(files ?? []), ...getTrashedFiles(updatedTrash)])
+ preservePhotoswipeProps(
+ sortFiles([
+ ...(files ?? []),
+ ...getTrashedFiles(updatedTrash),
+ ])
+ )
);
await localForage.setItem(TRASH, updatedTrash);
await localForage.setItem(TRASH_TIME, time);
diff --git a/src/services/updateCreationTimeWithExif.ts b/src/services/updateCreationTimeWithExif.ts
index e34f889c0..64b989fdc 100644
--- a/src/services/updateCreationTimeWithExif.ts
+++ b/src/services/updateCreationTimeWithExif.ts
@@ -10,9 +10,10 @@ import downloadManager from './downloadManager';
import { updatePublicMagicMetadata } from './fileService';
import { EnteFile } from 'types/file';
-import { getRawExif, getUNIXTime } from './upload/exifService';
+import { getRawExif } from './upload/exifService';
import { getFileType } from './upload/readFileService';
import { FILE_TYPE } from 'constants/file';
+import { getUnixTimeInMicroSeconds } from 'utils/time';
export async function updateCreationTimeWithExif(
filesToBeUpdated: EnteFile[],
@@ -33,19 +34,21 @@ export async function updateCreationTimeWithExif(
}
let correctCreationTime: number;
if (fixOption === FIX_OPTIONS.CUSTOM_TIME) {
- correctCreationTime = getUNIXTime(customTime);
+ correctCreationTime = getUnixTimeInMicroSeconds(customTime);
} else {
- const fileURL = await downloadManager.getFile(file);
+ const fileURL = await downloadManager.getFile(file)[0];
const fileObject = await getFileFromURL(fileURL);
const reader = new FileReader();
const fileTypeInfo = await getFileType(reader, fileObject);
const exifData = await getRawExif(fileObject, fileTypeInfo);
if (fixOption === FIX_OPTIONS.DATE_TIME_ORIGINAL) {
- correctCreationTime = getUNIXTime(
+ correctCreationTime = getUnixTimeInMicroSeconds(
exifData?.DateTimeOriginal
);
} else {
- correctCreationTime = getUNIXTime(exifData?.CreateDate);
+ correctCreationTime = getUnixTimeInMicroSeconds(
+ exifData?.CreateDate
+ );
}
}
if (
diff --git a/src/services/upload/exifService.ts b/src/services/upload/exifService.ts
index 685994235..ee81da515 100644
--- a/src/services/upload/exifService.ts
+++ b/src/services/upload/exifService.ts
@@ -1,9 +1,11 @@
-import { NULL_LOCATION } from 'constants/upload';
+import { NULL_EXTRACTED_METADATA, NULL_LOCATION } from 'constants/upload';
import { Location } from 'types/upload';
import exifr from 'exifr';
import piexif from 'piexifjs';
import { FileTypeInfo } from 'types/upload';
import { logError } from 'utils/sentry';
+import { ParsedExtractedMetadata } from 'types/upload';
+import { getUnixTimeInMicroSeconds } from 'utils/time';
const EXIF_TAGS_NEEDED = [
'DateTimeOriginal',
@@ -23,37 +25,29 @@ interface Exif {
GPSLatitudeRef?: number;
GPSLongitudeRef?: number;
}
-interface ParsedEXIFData {
- location: Location;
- creationTime: number;
-}
export async function getExifData(
receivedFile: File,
fileTypeInfo: FileTypeInfo
-): Promise {
- const nullExifData: ParsedEXIFData = {
- location: NULL_LOCATION,
- creationTime: null,
- };
+): Promise {
+ let parsedEXIFData = NULL_EXTRACTED_METADATA;
try {
const exifData = await getRawExif(receivedFile, fileTypeInfo);
if (!exifData) {
- return nullExifData;
+ return parsedEXIFData;
}
- const parsedEXIFData = {
+ parsedEXIFData = {
location: getEXIFLocation(exifData),
- creationTime: getUNIXTime(
+ creationTime: getUnixTimeInMicroSeconds(
exifData.DateTimeOriginal ??
exifData.CreateDate ??
exifData.ModifyDate
),
};
- return parsedEXIFData;
} catch (e) {
logError(e, 'getExifData failed');
- return nullExifData;
}
+ return parsedEXIFData;
}
export async function updateFileCreationDateInEXIF(
@@ -131,22 +125,6 @@ export async function getRawExif(
return exifData;
}
-export function getUNIXTime(dateTime: Date) {
- try {
- if (!dateTime) {
- return null;
- }
- const unixTime = dateTime.getTime() * 1000;
- if (unixTime <= 0) {
- return null;
- } else {
- return unixTime;
- }
- } catch (e) {
- logError(e, 'getUNIXTime failed', { dateTime });
- }
-}
-
function getEXIFLocation(exifData): Location {
if (!exifData.latitude || !exifData.longitude) {
return NULL_LOCATION;
diff --git a/src/services/upload/metadataService.ts b/src/services/upload/metadataService.ts
index 89564aee5..51985e460 100644
--- a/src/services/upload/metadataService.ts
+++ b/src/services/upload/metadataService.ts
@@ -6,9 +6,11 @@ import {
ParsedMetadataJSON,
Location,
FileTypeInfo,
+ ParsedExtractedMetadata,
} from 'types/upload';
-import { NULL_LOCATION } from 'constants/upload';
+import { NULL_EXTRACTED_METADATA, NULL_LOCATION } from 'constants/upload';
import { splitFilenameAndExtension } from 'utils/file';
+import { getVideoMetadata } from './videoMetadataService';
interface ParsedMetadataJSONWithTitle {
title: string;
@@ -25,23 +27,25 @@ export async function extractMetadata(
receivedFile: File,
fileTypeInfo: FileTypeInfo
) {
- let exifData = null;
+ let extractedMetadata: ParsedExtractedMetadata = NULL_EXTRACTED_METADATA;
if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) {
- exifData = await getExifData(receivedFile, fileTypeInfo);
+ extractedMetadata = await getExifData(receivedFile, fileTypeInfo);
+ } else if (fileTypeInfo.fileType === FILE_TYPE.VIDEO) {
+ extractedMetadata = await getVideoMetadata(receivedFile);
}
- const extractedMetadata: Metadata = {
+ const metadata: Metadata = {
title: `${splitFilenameAndExtension(receivedFile.name)[0]}.${
fileTypeInfo.exactType
}`,
creationTime:
- exifData?.creationTime ?? receivedFile.lastModified * 1000,
+ extractedMetadata.creationTime ?? receivedFile.lastModified * 1000,
modificationTime: receivedFile.lastModified * 1000,
- latitude: exifData?.location?.latitude,
- longitude: exifData?.location?.longitude,
+ latitude: extractedMetadata.location.latitude,
+ longitude: extractedMetadata.location.longitude,
fileType: fileTypeInfo.fileType,
};
- return extractedMetadata;
+ return metadata;
}
export const getMetadataJSONMapKey = (
diff --git a/src/services/upload/thumbnailService.ts b/src/services/upload/thumbnailService.ts
index b6f1d3903..baa1a9c61 100644
--- a/src/services/upload/thumbnailService.ts
+++ b/src/services/upload/thumbnailService.ts
@@ -15,7 +15,7 @@ const MAX_THUMBNAIL_SIZE = 100 * 1024;
const MIN_QUALITY = 0.5;
const MAX_QUALITY = 0.7;
-const WAIT_TIME_THUMBNAIL_GENERATION = 10 * 1000;
+const WAIT_TIME_THUMBNAIL_GENERATION = 30 * 1000;
interface Dimension {
width: number;
diff --git a/src/services/upload/uploadManager.ts b/src/services/upload/uploadManager.ts
index eeeceda0f..e40e83fca 100644
--- a/src/services/upload/uploadManager.ts
+++ b/src/services/upload/uploadManager.ts
@@ -5,7 +5,7 @@ import { getDedicatedCryptoWorker } from 'utils/crypto';
import {
sortFilesIntoCollections,
sortFiles,
- removeUnnecessaryFileProps,
+ preservePhotoswipeProps,
} from 'utils/file';
import { logError } from 'utils/sentry';
import { getMetadataJSONMapKey, parseMetadataJSON } from './metadataService';
@@ -239,10 +239,8 @@ class UploadManager {
if (fileUploadResult === FileUploadResults.UPLOADED) {
this.existingFiles.push(file);
this.existingFiles = sortFiles(this.existingFiles);
- await setLocalFiles(
- removeUnnecessaryFileProps(this.existingFiles)
- );
- this.setFiles(this.existingFiles);
+ await setLocalFiles(this.existingFiles);
+ this.setFiles(preservePhotoswipeProps(this.existingFiles));
if (!this.existingFilesCollectionWise.has(file.collectionID)) {
this.existingFilesCollectionWise.set(file.collectionID, []);
}
diff --git a/src/services/upload/videoMetadataService.ts b/src/services/upload/videoMetadataService.ts
new file mode 100644
index 000000000..0a13a5dc9
--- /dev/null
+++ b/src/services/upload/videoMetadataService.ts
@@ -0,0 +1,76 @@
+import { NULL_EXTRACTED_METADATA, NULL_LOCATION } from 'constants/upload';
+import ffmpegService from 'services/ffmpegService';
+import { getUnixTimeInMicroSeconds } from 'utils/time';
+import { ParsedExtractedMetadata } from 'types/upload';
+import { logError } from 'utils/sentry';
+
+enum VideoMetadata {
+ CREATION_TIME = 'creation_time',
+ APPLE_CONTENT_IDENTIFIER = 'com.apple.quicktime.content.identifier',
+ APPLE_LIVE_PHOTO_IDENTIFIER = 'com.apple.quicktime.live-photo.auto',
+ APPLE_CREATION_DATE = 'com.apple.quicktime.creationdate',
+ APPLE_LOCATION_ISO = 'com.apple.quicktime.location.ISO6709',
+ LOCATION = 'location',
+}
+
+export async function getVideoMetadata(file: File) {
+ let videoMetadata = NULL_EXTRACTED_METADATA;
+ try {
+ videoMetadata = await ffmpegService.extractMetadata(file);
+ } catch (e) {
+ logError(e, 'failed to get video metadata');
+ }
+
+ return videoMetadata;
+}
+
+export function parseFFmpegExtractedMetadata(encodedMetadata: Uint8Array) {
+ const metadataString = new TextDecoder().decode(encodedMetadata);
+ const metadataPropertyArray = metadataString.split('\n');
+ const metadataKeyValueArray = metadataPropertyArray.map((property) =>
+ property.split('=')
+ );
+ const validKeyValuePairs = metadataKeyValueArray.filter(
+ (keyValueArray) => keyValueArray.length === 2
+ ) as Array<[string, string]>;
+
+ const metadataMap = Object.fromEntries(validKeyValuePairs);
+
+ const location = parseAppleISOLocation(
+ metadataMap[VideoMetadata.APPLE_LOCATION_ISO] ??
+ metadataMap[VideoMetadata.LOCATION]
+ );
+
+ const creationTime = parseCreationTime(
+ metadataMap[VideoMetadata.APPLE_CREATION_DATE] ??
+ metadataMap[VideoMetadata.CREATION_TIME]
+ );
+ const parsedMetadata: ParsedExtractedMetadata = {
+ creationTime,
+ location: {
+ latitude: location.latitude,
+ longitude: location.longitude,
+ },
+ };
+ return parsedMetadata;
+}
+
+function parseAppleISOLocation(isoLocation: string) {
+ let location = NULL_LOCATION;
+ if (isoLocation) {
+ const [latitude, longitude] = isoLocation
+ .match(/(\+|-)\d+\.*\d+/g)
+ .map((x) => parseFloat(x));
+
+ location = { latitude, longitude };
+ }
+ return location;
+}
+
+function parseCreationTime(creationTime: string) {
+ let dateTime = null;
+ if (creationTime) {
+ dateTime = getUnixTimeInMicroSeconds(new Date(creationTime));
+ }
+ return dateTime;
+}
diff --git a/src/types/upload/index.ts b/src/types/upload/index.ts
index a49facddf..35a4692a6 100644
--- a/src/types/upload/index.ts
+++ b/src/types/upload/index.ts
@@ -130,3 +130,8 @@ export interface UploadFile extends BackupedFile {
encryptedKey: string;
keyDecryptionNonce: string;
}
+
+export interface ParsedExtractedMetadata {
+ location: Location;
+ creationTime: number;
+}
diff --git a/src/utils/billing/index.ts b/src/utils/billing/index.ts
index e8043d61d..b696665b7 100644
--- a/src/utils/billing/index.ts
+++ b/src/utils/billing/index.ts
@@ -9,6 +9,9 @@ import { CustomError } from '../error';
import { logError } from '../sentry';
const PAYMENT_PROVIDER_STRIPE = 'stripe';
+const PAYMENT_PROVIDER_APPSTORE = 'appstore';
+const PAYMENT_PROVIDER_PLAYSTORE = 'playstore';
+const PAYMENT_PROVIDER_PAYPAL = 'paypal';
const FREE_PLAN = 'free';
enum FAILURE_REASON {
@@ -96,6 +99,23 @@ export function hasStripeSubscription(subscription: Subscription) {
);
}
+export function hasMobileSubscription(subscription: Subscription) {
+ return (
+ hasPaidSubscription(subscription) &&
+ subscription.paymentProvider.length > 0 &&
+ (subscription.paymentProvider === PAYMENT_PROVIDER_APPSTORE ||
+ subscription.paymentProvider === PAYMENT_PROVIDER_PLAYSTORE)
+ );
+}
+
+export function hasPaypalSubscription(subscription: Subscription) {
+ return (
+ hasPaidSubscription(subscription) &&
+ subscription.paymentProvider.length > 0 &&
+ subscription.paymentProvider === PAYMENT_PROVIDER_PAYPAL
+ );
+}
+
export async function updateSubscription(
plan: Plan,
setDialogMessage: SetDialogMessage,
diff --git a/src/utils/file/index.ts b/src/utils/file/index.ts
index 9666a9c45..5ad062ed8 100644
--- a/src/utils/file/index.ts
+++ b/src/utils/file/index.ts
@@ -24,6 +24,7 @@ import {
} from 'constants/file';
import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager';
import HEICConverter from 'services/HEICConverter';
+import ffmpegService from 'services/ffmpegService';
export function downloadAsFile(filename: string, content: string) {
const file = new Blob([content], {
@@ -52,7 +53,7 @@ export async function downloadFile(
if (accessedThroughSharedURL) {
fileURL = await PublicCollectionDownloadManager.getCachedOriginalFile(
file
- );
+ )[0];
tempURL;
if (!fileURL) {
tempURL = URL.createObjectURL(
@@ -68,7 +69,7 @@ export async function downloadFile(
fileURL = tempURL;
}
} else {
- fileURL = await DownloadManager.getCachedOriginalFile(file);
+ fileURL = await DownloadManager.getCachedOriginalFile(file)[0];
if (!fileURL) {
tempURL = URL.createObjectURL(
await new Response(
@@ -281,20 +282,18 @@ export async function decryptFile(file: EnteFile, collectionKey: string) {
}
}
-export function removeUnnecessaryFileProps(files: EnteFile[]): EnteFile[] {
- const stripedFiles = files.map((file) => {
- delete file.src;
- delete file.msrc;
- delete file.file.objectKey;
- delete file.thumbnail.objectKey;
- delete file.h;
- delete file.html;
- delete file.w;
-
- return file;
- });
- return stripedFiles;
-}
+export const preservePhotoswipeProps =
+ (newFiles: EnteFile[]) =>
+ (currentFiles: EnteFile[]): EnteFile[] => {
+ const currentFilesMap = Object.fromEntries(
+ currentFiles.map((file) => [file.id, file])
+ );
+ const fileWithPreservedProperty = newFiles.map((file) => {
+ const currentFile = currentFilesMap[file.id];
+ return { ...currentFile, ...file };
+ });
+ return fileWithPreservedProperty;
+ };
export function fileNameWithoutExtension(filename) {
const lastDotPosition = filename.lastIndexOf('.');
@@ -331,23 +330,42 @@ export function generateStreamFromArrayBuffer(data: Uint8Array) {
});
}
-export async function convertForPreview(file: EnteFile, fileBlob: Blob) {
+export async function convertForPreview(
+ file: EnteFile,
+ fileBlob: Blob
+): Promise {
+ const convertIfHEIC = async (fileName: string, fileBlob: Blob) => {
+ const typeFromExtension = getFileExtension(fileName);
+ const reader = new FileReader();
+ const mimeType =
+ (await getFileTypeFromBlob(reader, fileBlob))?.mime ??
+ typeFromExtension;
+ if (isFileHEIC(mimeType)) {
+ fileBlob = await HEICConverter.convert(fileBlob);
+ }
+ return fileBlob;
+ };
+
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
const originalName = fileNameWithoutExtension(file.metadata.title);
const motionPhoto = await decodeMotionPhoto(fileBlob, originalName);
- fileBlob = new Blob([motionPhoto.image]);
+ let image = new Blob([motionPhoto.image]);
+
+ // can run conversion in parellel as video and image
+ // have different processes
+ const convertedVideo = ffmpegService.convertToMP4(
+ motionPhoto.video,
+ motionPhoto.videoNameTitle
+ );
+
+ image = await convertIfHEIC(motionPhoto.imageNameTitle, image);
+ const video = new Blob([await convertedVideo]);
+
+ return [image, video];
}
- const typeFromExtension = getFileExtension(file.metadata.title);
- const reader = new FileReader();
-
- const mimeType =
- (await getFileTypeFromBlob(reader, fileBlob))?.mime ??
- typeFromExtension;
- if (isFileHEIC(mimeType)) {
- fileBlob = await HEICConverter.convert(fileBlob);
- }
- return fileBlob;
+ fileBlob = await convertIfHEIC(file.metadata.title, fileBlob);
+ return [fileBlob];
}
export function fileIsArchived(file: EnteFile) {
diff --git a/src/utils/photoFrame/index.ts b/src/utils/photoFrame/index.ts
index fdaeb8557..fe5161440 100644
--- a/src/utils/photoFrame/index.ts
+++ b/src/utils/photoFrame/index.ts
@@ -13,3 +13,22 @@ export async function isPlaybackPossible(url: string): Promise {
video.src = url;
});
}
+
+export async function playVideo(livePhotoVideo, livePhotoImage) {
+ const videoPlaying = !livePhotoVideo.paused;
+ if (videoPlaying) return;
+ livePhotoVideo.style.opacity = 1;
+ livePhotoImage.style.opacity = 0;
+ livePhotoVideo.load();
+ livePhotoVideo.play().catch(() => {
+ pauseVideo(livePhotoVideo, livePhotoImage);
+ });
+}
+
+export async function pauseVideo(livePhotoVideo, livePhotoImage) {
+ const videoPlaying = !livePhotoVideo.paused;
+ if (!videoPlaying) return;
+ livePhotoVideo.pause();
+ livePhotoVideo.style.opacity = 0;
+ livePhotoImage.style.opacity = 1;
+}
diff --git a/src/utils/strings/englishConstants.tsx b/src/utils/strings/englishConstants.tsx
index 2096c570b..40d2fe5a0 100644
--- a/src/utils/strings/englishConstants.tsx
+++ b/src/utils/strings/englishConstants.tsx
@@ -104,7 +104,7 @@ const englishConstants = {
UPLOAD: {
0: 'preparing to upload',
1: 'reading google metadata files',
- 2: 'reading file metadata to organize file',
+ 2: 'reading file metadata',
3: (fileCounter) =>
`${fileCounter.finished} / ${fileCounter.total} files backed up`,
4: 'backup complete',
@@ -332,6 +332,14 @@ const englishConstants = {
SUBSCRIPTION_PURCHASE_SUCCESS_TITLE: 'thank you',
CANCEL_SUBSCRIPTION_ON_MOBILE:
'please cancel your subscription from the mobile app to activate a subscription here',
+ PAYPAL_MANAGE_NOT_SUPPORTED_MESSAGE: () => (
+ <>
+ please contact us at{' '}
+ paypal@ente.io to manage your
+ subscription
+ >
+ ),
+ PAYPAL_MANAGE_NOT_SUPPORTED: 'manage paypal plan',
RENAME: 'rename',
RENAME_COLLECTION: 'rename album',
CONFIRM_DELETE_COLLECTION: 'confirm album deletion',
@@ -674,6 +682,7 @@ const englishConstants = {
ENTE_IO: 'ente.io',
PLAYBACK_SUPPORT_COMING: 'playback support coming soon...',
LIVE_PHOTO: 'this is a live photo',
+ LIVE: 'LIVE',
};
export default englishConstants;
diff --git a/src/utils/time/index.ts b/src/utils/time/index.ts
index 320adab8f..65e15fbce 100644
--- a/src/utils/time/index.ts
+++ b/src/utils/time/index.ts
@@ -1,7 +1,11 @@
-export function getUTCMicroSecondsSinceEpoch(): number {
- const now = new Date();
- const utcMilllisecondsSinceEpoch =
- now.getTime() + now.getTimezoneOffset() * 60 * 1000;
- const utcSecondsSinceEpoch = Math.round(utcMilllisecondsSinceEpoch * 1000);
- return utcSecondsSinceEpoch;
+export function getUnixTimeInMicroSeconds(dateTime: Date) {
+ if (!dateTime || isNaN(dateTime.getTime())) {
+ return null;
+ }
+ const unixTime = dateTime.getTime() * 1000;
+ if (unixTime <= 0) {
+ return null;
+ } else {
+ return unixTime;
+ }
}