Merge branch 'master' into settings_button
This commit is contained in:
commit
0e32b2ac89
|
@ -17,8 +17,10 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
CSP_DIRECTIVES: {
|
CSP_DIRECTIVES: {
|
||||||
'default-src': "'none'",
|
// self is safe enough
|
||||||
'img-src': "'self' blob:",
|
'default-src': "'self'",
|
||||||
|
// data to allow two factor qr code
|
||||||
|
'img-src': "'self' blob: data:",
|
||||||
'media-src': "'self' blob:",
|
'media-src': "'self' blob:",
|
||||||
'manifest-src': "'self'",
|
'manifest-src': "'self'",
|
||||||
'style-src': "'self' 'unsafe-inline'",
|
'style-src': "'self' 'unsafe-inline'",
|
||||||
|
@ -26,10 +28,13 @@ module.exports = {
|
||||||
'connect-src':
|
'connect-src':
|
||||||
"'self' https://*.ente.io http://localhost:8080 data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com ",
|
"'self' https://*.ente.io http://localhost:8080 data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com ",
|
||||||
'base-uri ': "'self'",
|
'base-uri ': "'self'",
|
||||||
|
// to allow worker
|
||||||
|
'child-src': "'self' blob:",
|
||||||
|
'object-src': "'none'",
|
||||||
'frame-ancestors': " 'none'",
|
'frame-ancestors': " 'none'",
|
||||||
'form-action': "'none'",
|
'form-action': "'none'",
|
||||||
'report-uri': ' https://csp-reporter.ente.io',
|
'report-uri': ' https://csp-reporter.ente.io/local',
|
||||||
'report-to': ' https://csp-reporter.ente.io',
|
'report-to': ' https://csp-reporter.ente.io/local',
|
||||||
},
|
},
|
||||||
|
|
||||||
WORKBOX_CONFIG: {
|
WORKBOX_CONFIG: {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "bada-frame",
|
"name": "bada-frame",
|
||||||
"version": "0.7.0",
|
"version": "0.8.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|
|
@ -8,5 +8,5 @@
|
||||||
X-Frame-Options: deny
|
X-Frame-Options: deny
|
||||||
X-XSS-Protection: 1; mode=block
|
X-XSS-Protection: 1; mode=block
|
||||||
Referrer-Policy: same-origin
|
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;
|
||||||
|
|
||||||
|
|
12
src/components/LivePhotoBtn.tsx
Normal file
12
src/components/LivePhotoBtn.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
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>
|
||||||
|
);
|
|
@ -72,6 +72,11 @@ interface Props {
|
||||||
enableDownload: boolean;
|
enableDownload: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SourceURL = {
|
||||||
|
imageURL?: string;
|
||||||
|
videoURL?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const PhotoFrame = ({
|
const PhotoFrame = ({
|
||||||
files,
|
files,
|
||||||
setFiles,
|
setFiles,
|
||||||
|
@ -103,6 +108,7 @@ const PhotoFrame = ({
|
||||||
const filteredDataRef = useRef([]);
|
const filteredDataRef = useRef([]);
|
||||||
const filteredData = filteredDataRef?.current ?? [];
|
const filteredData = filteredDataRef?.current ?? [];
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [isSourceLoaded, setIsSourceLoaded] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Shift') {
|
if (e.key === 'Shift') {
|
||||||
|
@ -244,18 +250,15 @@ const PhotoFrame = ({
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
const updateURL = (index: number) => (url: string) => {
|
const updateURL = (index: number) => (url: string) => {
|
||||||
files[index] = {
|
const updateFile = (file: EnteFile) => {
|
||||||
...files[index],
|
file = {
|
||||||
msrc: url,
|
...file,
|
||||||
src: files[index].src ? files[index].src : url,
|
msrc: url,
|
||||||
w: window.innerWidth,
|
w: window.innerWidth,
|
||||||
h: window.innerHeight,
|
h: window.innerHeight,
|
||||||
};
|
};
|
||||||
if (
|
if (file.metadata.fileType === FILE_TYPE.VIDEO && !file.html) {
|
||||||
files[index].metadata.fileType === FILE_TYPE.VIDEO &&
|
file.html = `
|
||||||
!files[index].html
|
|
||||||
) {
|
|
||||||
files[index].html = `
|
|
||||||
<div class="video-loading">
|
<div class="video-loading">
|
||||||
<img src="${url}" />
|
<img src="${url}" />
|
||||||
<div class="spinner-border text-light" role="status">
|
<div class="spinner-border text-light" role="status">
|
||||||
|
@ -263,46 +266,94 @@ const PhotoFrame = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
delete files[index].src;
|
} else if (
|
||||||
}
|
file.metadata.fileType === FILE_TYPE.LIVE_PHOTO &&
|
||||||
if (
|
!file.html
|
||||||
files[index].metadata.fileType === FILE_TYPE.IMAGE &&
|
) {
|
||||||
!files[index].src
|
file.html = `
|
||||||
) {
|
<div class="video-loading">
|
||||||
files[index].src = url;
|
<img src="${url}" />
|
||||||
}
|
<div class="spinner-border text-light" role="status">
|
||||||
setFiles(files);
|
<span class="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} 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) => {
|
const updateSrcURL = async (index: number, srcURL: SourceURL) => {
|
||||||
files[index] = {
|
const { videoURL, imageURL } = srcURL;
|
||||||
...files[index],
|
const isPlayable = videoURL && (await isPlaybackPossible(videoURL));
|
||||||
w: window.innerWidth,
|
const updateFile = (file: EnteFile) => {
|
||||||
h: window.innerHeight,
|
file = {
|
||||||
};
|
...file,
|
||||||
if (files[index].metadata.fileType === FILE_TYPE.VIDEO) {
|
w: window.innerWidth,
|
||||||
if (await isPlaybackPossible(url)) {
|
h: window.innerHeight,
|
||||||
files[index].html = `
|
};
|
||||||
<video controls>
|
if (file.metadata.fileType === FILE_TYPE.VIDEO) {
|
||||||
<source src="${url}" />
|
if (isPlayable) {
|
||||||
Your browser does not support the video tag.
|
file.html = `
|
||||||
</video>
|
<video controls>
|
||||||
|
<source src="${videoURL}" />
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
file.html = `
|
||||||
|
<div class="video-loading">
|
||||||
|
<img src="${file.msrc}" />
|
||||||
|
<div class="download-message" >
|
||||||
|
${constants.VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD}
|
||||||
|
<a class="btn btn-outline-success" href=${videoURL} download="${file.metadata.title}"">Download</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
} else {
|
}
|
||||||
files[index].html = `
|
} else if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||||
|
if (isPlayable) {
|
||||||
|
file.html = `
|
||||||
|
<div class = 'live-photo-container'>
|
||||||
|
<img id = "live-photo-image-${file.id}" src="${imageURL}" />
|
||||||
|
<video id = "live-photo-video-${file.id}" loop muted>
|
||||||
|
<source src="${videoURL}" />
|
||||||
|
Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
file.html = `
|
||||||
<div class="video-loading">
|
<div class="video-loading">
|
||||||
<img src="${files[index].msrc}" />
|
<img src="${file.msrc}" />
|
||||||
<div class="download-message" >
|
<div class="download-message">
|
||||||
${constants.VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD}
|
${constants.VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD}
|
||||||
<a class="btn btn-outline-success" href=${url} download="${files[index].metadata.title}"">Download</button>
|
<button class = "btn btn-outline-success" id = "download-btn-${file.id}">Download</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
file.src = imageURL;
|
||||||
}
|
}
|
||||||
} else {
|
return file;
|
||||||
files[index].src = url;
|
};
|
||||||
}
|
setFiles((files) => {
|
||||||
setFiles(files);
|
files[index] = updateFile(files[index]);
|
||||||
|
return [...files];
|
||||||
|
});
|
||||||
|
setIsSourceLoaded(true);
|
||||||
|
return updateFile(files[index]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = (needUpdate) => {
|
const handleClose = (needUpdate) => {
|
||||||
|
@ -418,13 +469,13 @@ const PhotoFrame = ({
|
||||||
}
|
}
|
||||||
galleryContext.thumbs.set(item.id, url);
|
galleryContext.thumbs.set(item.id, url);
|
||||||
}
|
}
|
||||||
updateURL(item.dataIndex)(url);
|
const newFile = updateURL(item.dataIndex)(url);
|
||||||
item.msrc = url;
|
item.msrc = newFile.msrc;
|
||||||
if (!item.src) {
|
item.html = newFile.html;
|
||||||
item.src = url;
|
item.src = newFile.src;
|
||||||
}
|
item.w = newFile.w;
|
||||||
item.w = window.innerWidth;
|
item.h = newFile.h;
|
||||||
item.h = window.innerHeight;
|
|
||||||
try {
|
try {
|
||||||
instance.invalidateCurrItems();
|
instance.invalidateCurrItems();
|
||||||
instance.updateSize(true);
|
instance.updateSize(true);
|
||||||
|
@ -438,29 +489,47 @@ const PhotoFrame = ({
|
||||||
if (!fetching[item.dataIndex]) {
|
if (!fetching[item.dataIndex]) {
|
||||||
try {
|
try {
|
||||||
fetching[item.dataIndex] = true;
|
fetching[item.dataIndex] = true;
|
||||||
let url: string;
|
let urls: string[];
|
||||||
if (galleryContext.files.has(item.id)) {
|
if (galleryContext.files.has(item.id)) {
|
||||||
url = galleryContext.files.get(item.id);
|
const mergedURL = galleryContext.files.get(item.id);
|
||||||
|
urls = mergedURL.split(',');
|
||||||
} else {
|
} else {
|
||||||
|
galleryContext.startLoading();
|
||||||
if (
|
if (
|
||||||
publicCollectionGalleryContext.accessedThroughSharedURL
|
publicCollectionGalleryContext.accessedThroughSharedURL
|
||||||
) {
|
) {
|
||||||
url = await PublicCollectionDownloadManager.getFile(
|
urls = await PublicCollectionDownloadManager.getFile(
|
||||||
item,
|
item,
|
||||||
publicCollectionGalleryContext.token,
|
publicCollectionGalleryContext.token,
|
||||||
publicCollectionGalleryContext.passwordToken,
|
publicCollectionGalleryContext.passwordToken,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
} else {
|
} 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);
|
let imageURL;
|
||||||
item.html = files[item.dataIndex].html;
|
let videoURL;
|
||||||
item.src = files[item.dataIndex].src;
|
if (item.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||||
item.w = files[item.dataIndex].w;
|
[imageURL, videoURL] = urls;
|
||||||
item.h = files[item.dataIndex].h;
|
} 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 {
|
try {
|
||||||
instance.invalidateCurrItems();
|
instance.invalidateCurrItems();
|
||||||
instance.updateSize(true);
|
instance.updateSize(true);
|
||||||
|
@ -524,6 +593,7 @@ const PhotoFrame = ({
|
||||||
isSharedCollection={isSharedCollection}
|
isSharedCollection={isSharedCollection}
|
||||||
isTrashCollection={activeCollection === TRASH_SECTION}
|
isTrashCollection={activeCollection === TRASH_SECTION}
|
||||||
enableDownload={enableDownload}
|
enableDownload={enableDownload}
|
||||||
|
isSourceLoaded={isSourceLoaded}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -20,7 +20,6 @@ import {
|
||||||
changeFileName,
|
changeFileName,
|
||||||
downloadFile,
|
downloadFile,
|
||||||
formatDateTime,
|
formatDateTime,
|
||||||
isLivePhoto,
|
|
||||||
splitFilenameAndExtension,
|
splitFilenameAndExtension,
|
||||||
updateExistingFilePubMetadata,
|
updateExistingFilePubMetadata,
|
||||||
} from 'utils/file';
|
} from 'utils/file';
|
||||||
|
@ -35,6 +34,7 @@ import {
|
||||||
Row,
|
Row,
|
||||||
Value,
|
Value,
|
||||||
} from 'components/Container';
|
} from 'components/Container';
|
||||||
|
import { livePhotoBtnHTML } from 'components/LivePhotoBtn';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
|
|
||||||
import CloseIcon from 'components/icons/CloseIcon';
|
import CloseIcon from 'components/icons/CloseIcon';
|
||||||
|
@ -43,14 +43,11 @@ import { Formik } from 'formik';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import EnteSpinner from 'components/EnteSpinner';
|
import EnteSpinner from 'components/EnteSpinner';
|
||||||
import EnteDateTimePicker from 'components/EnteDateTimePicker';
|
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 { sleep } from 'utils/common';
|
||||||
|
import { playVideo, pauseVideo } from 'utils/photoFrame';
|
||||||
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
||||||
import { GalleryContext } from 'pages/gallery';
|
import { GalleryContext } from 'pages/gallery';
|
||||||
import {
|
|
||||||
getLivePhotoInfoShownCount,
|
|
||||||
setLivePhotoInfoShownCount,
|
|
||||||
} from 'utils/storage';
|
|
||||||
|
|
||||||
const SmallLoadingSpinner = () => (
|
const SmallLoadingSpinner = () => (
|
||||||
<EnteSpinner
|
<EnteSpinner
|
||||||
|
@ -72,6 +69,7 @@ interface Iprops {
|
||||||
isSharedCollection: boolean;
|
isSharedCollection: boolean;
|
||||||
isTrashCollection: boolean;
|
isTrashCollection: boolean;
|
||||||
enableDownload: boolean;
|
enableDownload: boolean;
|
||||||
|
isSourceLoaded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LegendContainer = styled.div`
|
const LegendContainer = styled.div`
|
||||||
|
@ -90,6 +88,29 @@ const Pre = styled.pre`
|
||||||
padding: 7px 15px;
|
padding: 7px 15px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const LivePhotoBtn = styled.button`
|
||||||
|
position: absolute;
|
||||||
|
bottom: 6vh;
|
||||||
|
right: 6vh;
|
||||||
|
height: 40px;
|
||||||
|
width: 80px;
|
||||||
|
background: #d7d7d7;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10%;
|
||||||
|
z-index: 10;
|
||||||
|
cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const livePhotoDefaultOptions = {
|
||||||
|
click: () => {},
|
||||||
|
hide: () => {},
|
||||||
|
show: () => {},
|
||||||
|
loading: false,
|
||||||
|
visible: false,
|
||||||
|
};
|
||||||
|
|
||||||
const renderInfoItem = (label: string, value: string | JSX.Element) => (
|
const renderInfoItem = (label: string, value: string | JSX.Element) => (
|
||||||
<Row>
|
<Row>
|
||||||
<Label width="30%">{label}</Label>
|
<Label width="30%">{label}</Label>
|
||||||
|
@ -490,13 +511,17 @@ function InfoModal({
|
||||||
|
|
||||||
function PhotoSwipe(props: Iprops) {
|
function PhotoSwipe(props: Iprops) {
|
||||||
const pswpElement = useRef<HTMLDivElement>();
|
const pswpElement = useRef<HTMLDivElement>();
|
||||||
const [photoSwipe, setPhotoSwipe] = useState<Photoswipe<any>>();
|
const [photoSwipe, setPhotoSwipe] =
|
||||||
|
useState<Photoswipe<Photoswipe.Options>>();
|
||||||
|
|
||||||
const { isOpen, items } = props;
|
const { isOpen, items, isSourceLoaded } = props;
|
||||||
const [isFav, setIsFav] = useState(false);
|
const [isFav, setIsFav] = useState(false);
|
||||||
const [showInfo, setShowInfo] = useState(false);
|
const [showInfo, setShowInfo] = useState(false);
|
||||||
const [metadata, setMetaData] = useState<EnteFile['metadata']>(null);
|
const [metadata, setMetaData] = useState<EnteFile['metadata']>(null);
|
||||||
const [exif, setExif] = useState<any>(null);
|
const [exif, setExif] = useState<any>(null);
|
||||||
|
const [livePhotoBtnOptions, setLivePhotoBtnOptions] = useState(
|
||||||
|
livePhotoDefaultOptions
|
||||||
|
);
|
||||||
const needUpdate = useRef(false);
|
const needUpdate = useRef(false);
|
||||||
const publicCollectionGalleryContext = useContext(
|
const publicCollectionGalleryContext = useContext(
|
||||||
PublicCollectionGalleryContext
|
PublicCollectionGalleryContext
|
||||||
|
@ -527,23 +552,75 @@ function PhotoSwipe(props: Iprops) {
|
||||||
}
|
}
|
||||||
}, [showInfo]);
|
}, [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() {
|
function updateFavButton() {
|
||||||
setIsFav(isInFav(this?.currItem));
|
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 openPhotoSwipe = () => {
|
||||||
const { items, currentIndex } = props;
|
const { items, currentIndex } = props;
|
||||||
const options = {
|
const options = {
|
||||||
|
@ -606,7 +683,6 @@ function PhotoSwipe(props: Iprops) {
|
||||||
photoSwipe.listen('beforeChange', function () {
|
photoSwipe.listen('beforeChange', function () {
|
||||||
updateInfo.call(this);
|
updateInfo.call(this);
|
||||||
updateFavButton.call(this);
|
updateFavButton.call(this);
|
||||||
handleLivePhotoNotification.call(this);
|
|
||||||
});
|
});
|
||||||
photoSwipe.listen('resize', checkExifAvailable);
|
photoSwipe.listen('resize', checkExifAvailable);
|
||||||
photoSwipe.init();
|
photoSwipe.init();
|
||||||
|
@ -726,6 +802,18 @@ function PhotoSwipe(props: Iprops) {
|
||||||
ref={pswpElement}>
|
ref={pswpElement}>
|
||||||
<div className="pswp__bg" />
|
<div className="pswp__bg" />
|
||||||
<div className="pswp__scroll-wrap">
|
<div className="pswp__scroll-wrap">
|
||||||
|
<LivePhotoBtn
|
||||||
|
onClick={livePhotoBtnOptions.click}
|
||||||
|
onMouseEnter={livePhotoBtnOptions.show}
|
||||||
|
onMouseLeave={livePhotoBtnOptions.hide}
|
||||||
|
disabled={livePhotoBtnOptions.loading}
|
||||||
|
style={{
|
||||||
|
display: livePhotoBtnOptions.visible
|
||||||
|
? 'block'
|
||||||
|
: 'none',
|
||||||
|
}}>
|
||||||
|
{livePhotoBtnHTML} {constants.LIVE}
|
||||||
|
</LivePhotoBtn>
|
||||||
<div className="pswp__container">
|
<div className="pswp__container">
|
||||||
<div className="pswp__item" />
|
<div className="pswp__item" />
|
||||||
<div className="pswp__item" />
|
<div className="pswp__item" />
|
||||||
|
|
|
@ -16,6 +16,8 @@ import {
|
||||||
hasPaidSubscription,
|
hasPaidSubscription,
|
||||||
isOnFreePlan,
|
isOnFreePlan,
|
||||||
planForSubscription,
|
planForSubscription,
|
||||||
|
hasMobileSubscription,
|
||||||
|
hasPaypalSubscription,
|
||||||
} from 'utils/billing';
|
} from 'utils/billing';
|
||||||
import { reverseString } from 'utils/common';
|
import { reverseString } from 'utils/common';
|
||||||
import { SetDialogMessage } from 'components/MessageDialog';
|
import { SetDialogMessage } from 'components/MessageDialog';
|
||||||
|
@ -143,8 +145,7 @@ function PlanSelector(props: Props) {
|
||||||
|
|
||||||
async function onPlanSelect(plan: Plan) {
|
async function onPlanSelect(plan: Plan) {
|
||||||
if (
|
if (
|
||||||
hasPaidSubscription(subscription) &&
|
hasMobileSubscription(subscription) &&
|
||||||
!hasStripeSubscription(subscription) &&
|
|
||||||
!isSubscriptionCancelled(subscription)
|
!isSubscriptionCancelled(subscription)
|
||||||
) {
|
) {
|
||||||
props.setDialogMessage({
|
props.setDialogMessage({
|
||||||
|
@ -152,6 +153,15 @@ function PlanSelector(props: Props) {
|
||||||
content: constants.CANCEL_SUBSCRIPTION_ON_MOBILE,
|
content: constants.CANCEL_SUBSCRIPTION_ON_MOBILE,
|
||||||
close: { variant: 'danger' },
|
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)) {
|
} else if (hasStripeSubscription(subscription)) {
|
||||||
props.setDialogMessage({
|
props.setDialogMessage({
|
||||||
title: `${constants.CONFIRM} ${reverseString(
|
title: `${constants.CONFIRM} ${reverseString(
|
||||||
|
|
|
@ -164,7 +164,7 @@ const Cont = styled.div<{ disabled: boolean; selected: boolean }>`
|
||||||
|
|
||||||
export default function PreviewCard(props: IProps) {
|
export default function PreviewCard(props: IProps) {
|
||||||
const [imgSrc, setImgSrc] = useState<string>();
|
const [imgSrc, setImgSrc] = useState<string>();
|
||||||
const { thumbs, files } = useContext(GalleryContext);
|
const { thumbs } = useContext(GalleryContext);
|
||||||
const {
|
const {
|
||||||
file,
|
file,
|
||||||
onClick,
|
onClick,
|
||||||
|
@ -203,10 +203,6 @@ export default function PreviewCard(props: IProps) {
|
||||||
if (isMounted.current) {
|
if (isMounted.current) {
|
||||||
setImgSrc(url);
|
setImgSrc(url);
|
||||||
thumbs.set(file.id, url);
|
thumbs.set(file.id, url);
|
||||||
file.msrc = url;
|
|
||||||
if (!file.src) {
|
|
||||||
file.src = url;
|
|
||||||
}
|
|
||||||
updateURL(url);
|
updateURL(url);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -218,13 +214,6 @@ export default function PreviewCard(props: IProps) {
|
||||||
const thumbImgSrc = thumbs.get(file.id);
|
const thumbImgSrc = thumbs.get(file.id);
|
||||||
setImgSrc(thumbImgSrc);
|
setImgSrc(thumbImgSrc);
|
||||||
file.msrc = thumbImgSrc;
|
file.msrc = thumbImgSrc;
|
||||||
if (!file.src) {
|
|
||||||
if (files.has(file.id)) {
|
|
||||||
file.src = files.get(file.id);
|
|
||||||
} else {
|
|
||||||
file.src = thumbImgSrc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
main();
|
main();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { ENCRYPTION_CHUNK_SIZE } from 'constants/crypto';
|
import { ENCRYPTION_CHUNK_SIZE } from 'constants/crypto';
|
||||||
import { FILE_TYPE } from 'constants/file';
|
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.
|
// list of format that were missed by type-detection for some files.
|
||||||
export const FORMAT_MISSED_BY_FILE_TYPE_LIB = [
|
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 MAX_FILE_SIZE_SUPPORTED = 5 * 1024 * 1024 * 1024; // 5 GB
|
||||||
|
|
||||||
export const LIVE_PHOTO_ASSET_SIZE_LIMIT = 20 * 1024 * 1024; // 20MB
|
export const LIVE_PHOTO_ASSET_SIZE_LIMIT = 20 * 1024 * 1024; // 20MB
|
||||||
|
|
||||||
|
export const NULL_EXTRACTED_METADATA: ParsedExtractedMetadata = {
|
||||||
|
location: NULL_LOCATION,
|
||||||
|
creationTime: null,
|
||||||
|
};
|
||||||
|
|
|
@ -69,6 +69,35 @@ const GlobalStyles = createGlobalStyle`
|
||||||
height: 100%;
|
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 {
|
.video-loading {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { FILE_TYPE } from 'constants/file';
|
||||||
import { CustomError } from 'utils/error';
|
import { CustomError } from 'utils/error';
|
||||||
|
|
||||||
class DownloadManager {
|
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 getThumbnail(file: EnteFile) {
|
public async getThumbnail(file: EnteFile) {
|
||||||
|
@ -90,11 +90,17 @@ class DownloadManager {
|
||||||
try {
|
try {
|
||||||
const getFilePromise = async (convert: boolean) => {
|
const getFilePromise = async (convert: boolean) => {
|
||||||
const fileStream = await this.downloadFile(file);
|
const fileStream = await this.downloadFile(file);
|
||||||
let fileBlob = await new Response(fileStream).blob();
|
const fileBlob = await new Response(fileStream).blob();
|
||||||
if (convert) {
|
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)) {
|
if (!this.fileObjectURLPromise.get(fileKey)) {
|
||||||
this.fileObjectURLPromise.set(
|
this.fileObjectURLPromise.set(
|
||||||
|
@ -102,8 +108,8 @@ class DownloadManager {
|
||||||
getFilePromise(shouldBeConverted)
|
getFilePromise(shouldBeConverted)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const fileURL = await this.fileObjectURLPromise.get(fileKey);
|
const fileURLs = await this.fileObjectURLPromise.get(fileKey);
|
||||||
return fileURL;
|
return fileURLs;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.fileObjectURLPromise.delete(fileKey);
|
this.fileObjectURLPromise.delete(fileKey);
|
||||||
logError(e, 'Failed to get File');
|
logError(e, 'Failed to get File');
|
||||||
|
|
|
@ -2,14 +2,17 @@ import { createFFmpeg, FFmpeg } from '@ffmpeg/ffmpeg';
|
||||||
import { CustomError } from 'utils/error';
|
import { CustomError } from 'utils/error';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import QueueProcessor from './queueProcessor';
|
import QueueProcessor from './queueProcessor';
|
||||||
|
import { ParsedExtractedMetadata } from 'types/upload';
|
||||||
|
|
||||||
import { getUint8ArrayView } from './upload/readFileService';
|
import { getUint8ArrayView } from './upload/readFileService';
|
||||||
|
import { parseFFmpegExtractedMetadata } from './upload/videoMetadataService';
|
||||||
|
|
||||||
class FFmpegService {
|
class FFmpegService {
|
||||||
private ffmpeg: FFmpeg = null;
|
private ffmpeg: FFmpeg = null;
|
||||||
private isLoading = null;
|
private isLoading = null;
|
||||||
private fileReader: FileReader = null;
|
private fileReader: FileReader = null;
|
||||||
|
|
||||||
private generateThumbnailProcessor = new QueueProcessor<Uint8Array>(1);
|
private ffmpegTaskQueue = new QueueProcessor<any>(1);
|
||||||
async init() {
|
async init() {
|
||||||
try {
|
try {
|
||||||
this.ffmpeg = createFFmpeg({
|
this.ffmpeg = createFFmpeg({
|
||||||
|
@ -26,7 +29,7 @@ class FFmpegService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateThumbnail(file: File) {
|
async generateThumbnail(file: File): Promise<Uint8Array> {
|
||||||
if (!this.ffmpeg) {
|
if (!this.ffmpeg) {
|
||||||
await this.init();
|
await this.init();
|
||||||
}
|
}
|
||||||
|
@ -36,7 +39,7 @@ class FFmpegService {
|
||||||
if (this.isLoading) {
|
if (this.isLoading) {
|
||||||
await this.isLoading;
|
await this.isLoading;
|
||||||
}
|
}
|
||||||
const response = this.generateThumbnailProcessor.queueUpRequest(
|
const response = this.ffmpegTaskQueue.queueUpRequest(
|
||||||
generateThumbnailHelper.bind(
|
generateThumbnailHelper.bind(
|
||||||
null,
|
null,
|
||||||
this.ffmpeg,
|
this.ffmpeg,
|
||||||
|
@ -56,6 +59,65 @@ class FFmpegService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async extractMetadata(file: File): Promise<ParsedExtractedMetadata> {
|
||||||
|
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<Uint8Array> {
|
||||||
|
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(
|
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();
|
export default new FFmpegService();
|
||||||
|
|
|
@ -6,9 +6,15 @@ import { EncryptionResult } from 'types/upload';
|
||||||
import { Collection } from 'types/collection';
|
import { Collection } from 'types/collection';
|
||||||
import HTTPService from './HTTPService';
|
import HTTPService from './HTTPService';
|
||||||
import { logError } from 'utils/sentry';
|
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 CryptoWorker from 'utils/crypto';
|
||||||
import { EnteFile, TrashRequest, UpdateMagicMetadataRequest } from 'types/file';
|
import { EnteFile, TrashRequest, UpdateMagicMetadataRequest } from 'types/file';
|
||||||
|
import { SetFiles } from 'types/gallery';
|
||||||
|
|
||||||
const ENDPOINT = getEndpoint();
|
const ENDPOINT = getEndpoint();
|
||||||
const FILES_TABLE = 'files';
|
const FILES_TABLE = 'files';
|
||||||
|
@ -28,13 +34,13 @@ const getCollectionLastSyncTime = async (collection: Collection) =>
|
||||||
|
|
||||||
export const syncFiles = async (
|
export const syncFiles = async (
|
||||||
collections: Collection[],
|
collections: Collection[],
|
||||||
setFiles: (files: EnteFile[]) => void
|
setFiles: SetFiles
|
||||||
) => {
|
) => {
|
||||||
const localFiles = await getLocalFiles();
|
const localFiles = await getLocalFiles();
|
||||||
let files = await removeDeletedCollectionFiles(collections, localFiles);
|
let files = await removeDeletedCollectionFiles(collections, localFiles);
|
||||||
if (files.length !== localFiles.length) {
|
if (files.length !== localFiles.length) {
|
||||||
await setLocalFiles(files);
|
await setLocalFiles(files);
|
||||||
setFiles([...sortFiles(mergeMetadata(files))]);
|
setFiles(preservePhotoswipeProps([...sortFiles(mergeMetadata(files))]));
|
||||||
}
|
}
|
||||||
for (const collection of collections) {
|
for (const collection of collections) {
|
||||||
if (!getToken()) {
|
if (!getToken()) {
|
||||||
|
@ -70,7 +76,7 @@ export const syncFiles = async (
|
||||||
`${collection.id}-time`,
|
`${collection.id}-time`,
|
||||||
collection.updationTime
|
collection.updationTime
|
||||||
);
|
);
|
||||||
setFiles([...sortFiles(mergeMetadata(files))]);
|
setFiles(preservePhotoswipeProps([...sortFiles(mergeMetadata(files))]));
|
||||||
}
|
}
|
||||||
return sortFiles(mergeMetadata(files));
|
return sortFiles(mergeMetadata(files));
|
||||||
};
|
};
|
||||||
|
@ -79,7 +85,7 @@ export const getFiles = async (
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
sinceTime: number,
|
sinceTime: number,
|
||||||
files: EnteFile[],
|
files: EnteFile[],
|
||||||
setFiles: (files: EnteFile[]) => void
|
setFiles: SetFiles
|
||||||
): Promise<EnteFile[]> => {
|
): Promise<EnteFile[]> => {
|
||||||
try {
|
try {
|
||||||
const decryptedFiles: EnteFile[] = [];
|
const decryptedFiles: EnteFile[] = [];
|
||||||
|
@ -116,10 +122,12 @@ export const getFiles = async (
|
||||||
time = resp.data.diff.slice(-1)[0].updationTime;
|
time = resp.data.diff.slice(-1)[0].updationTime;
|
||||||
}
|
}
|
||||||
setFiles(
|
setFiles(
|
||||||
sortFiles(
|
preservePhotoswipeProps(
|
||||||
mergeMetadata(
|
sortFiles(
|
||||||
[...(files || []), ...decryptedFiles].filter(
|
mergeMetadata(
|
||||||
(item) => !item.isDeleted
|
[...(files || []), ...decryptedFiles].filter(
|
||||||
|
(item) => !item.isDeleted
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { FILE_TYPE } from 'constants/file';
|
||||||
import { CustomError } from 'utils/error';
|
import { CustomError } from 'utils/error';
|
||||||
|
|
||||||
class PublicCollectionDownloadManager {
|
class PublicCollectionDownloadManager {
|
||||||
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 getThumbnail(
|
public async getThumbnail(
|
||||||
|
@ -116,11 +116,17 @@ class PublicCollectionDownloadManager {
|
||||||
passwordToken,
|
passwordToken,
|
||||||
file
|
file
|
||||||
);
|
);
|
||||||
let fileBlob = await new Response(fileStream).blob();
|
const fileBlob = await new Response(fileStream).blob();
|
||||||
if (convert) {
|
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)) {
|
if (!this.fileObjectURLPromise.get(fileKey)) {
|
||||||
this.fileObjectURLPromise.set(
|
this.fileObjectURLPromise.set(
|
||||||
|
@ -128,8 +134,8 @@ class PublicCollectionDownloadManager {
|
||||||
getFilePromise(shouldBeConverted)
|
getFilePromise(shouldBeConverted)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const fileURL = await this.fileObjectURLPromise.get(fileKey);
|
const fileURLs = await this.fileObjectURLPromise.get(fileKey);
|
||||||
return fileURL;
|
return fileURLs;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.fileObjectURLPromise.delete(fileKey);
|
this.fileObjectURLPromise.delete(fileKey);
|
||||||
logError(e, 'Failed to get File');
|
logError(e, 'Failed to get File');
|
||||||
|
|
|
@ -2,7 +2,12 @@ import { SetFiles } from 'types/gallery';
|
||||||
import { Collection } from 'types/collection';
|
import { Collection } from 'types/collection';
|
||||||
import { getEndpoint } from 'utils/common/apiUtil';
|
import { getEndpoint } from 'utils/common/apiUtil';
|
||||||
import { getToken } from 'utils/common/key';
|
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 { logError } from 'utils/sentry';
|
||||||
import localForage from 'utils/storage/localForage';
|
import localForage from 'utils/storage/localForage';
|
||||||
import { getCollection } from './collectionService';
|
import { getCollection } from './collectionService';
|
||||||
|
@ -120,7 +125,12 @@ export const updateTrash = async (
|
||||||
updatedTrash = removeRestoredOrDeletedTrashItems(updatedTrash);
|
updatedTrash = removeRestoredOrDeletedTrashItems(updatedTrash);
|
||||||
|
|
||||||
setFiles(
|
setFiles(
|
||||||
sortFiles([...(files ?? []), ...getTrashedFiles(updatedTrash)])
|
preservePhotoswipeProps(
|
||||||
|
sortFiles([
|
||||||
|
...(files ?? []),
|
||||||
|
...getTrashedFiles(updatedTrash),
|
||||||
|
])
|
||||||
|
)
|
||||||
);
|
);
|
||||||
await localForage.setItem(TRASH, updatedTrash);
|
await localForage.setItem(TRASH, updatedTrash);
|
||||||
await localForage.setItem(TRASH_TIME, time);
|
await localForage.setItem(TRASH_TIME, time);
|
||||||
|
|
|
@ -10,9 +10,10 @@ import downloadManager from './downloadManager';
|
||||||
import { updatePublicMagicMetadata } from './fileService';
|
import { updatePublicMagicMetadata } from './fileService';
|
||||||
import { EnteFile } from 'types/file';
|
import { EnteFile } from 'types/file';
|
||||||
|
|
||||||
import { getRawExif, getUNIXTime } from './upload/exifService';
|
import { getRawExif } from './upload/exifService';
|
||||||
import { getFileType } from './upload/readFileService';
|
import { getFileType } from './upload/readFileService';
|
||||||
import { FILE_TYPE } from 'constants/file';
|
import { FILE_TYPE } from 'constants/file';
|
||||||
|
import { getUnixTimeInMicroSeconds } from 'utils/time';
|
||||||
|
|
||||||
export async function updateCreationTimeWithExif(
|
export async function updateCreationTimeWithExif(
|
||||||
filesToBeUpdated: EnteFile[],
|
filesToBeUpdated: EnteFile[],
|
||||||
|
@ -33,19 +34,21 @@ export async function updateCreationTimeWithExif(
|
||||||
}
|
}
|
||||||
let correctCreationTime: number;
|
let correctCreationTime: number;
|
||||||
if (fixOption === FIX_OPTIONS.CUSTOM_TIME) {
|
if (fixOption === FIX_OPTIONS.CUSTOM_TIME) {
|
||||||
correctCreationTime = getUNIXTime(customTime);
|
correctCreationTime = getUnixTimeInMicroSeconds(customTime);
|
||||||
} else {
|
} else {
|
||||||
const fileURL = await downloadManager.getFile(file);
|
const fileURL = await downloadManager.getFile(file)[0];
|
||||||
const fileObject = await getFileFromURL(fileURL);
|
const fileObject = await getFileFromURL(fileURL);
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
const fileTypeInfo = await getFileType(reader, fileObject);
|
const fileTypeInfo = await getFileType(reader, fileObject);
|
||||||
const exifData = await getRawExif(fileObject, fileTypeInfo);
|
const exifData = await getRawExif(fileObject, fileTypeInfo);
|
||||||
if (fixOption === FIX_OPTIONS.DATE_TIME_ORIGINAL) {
|
if (fixOption === FIX_OPTIONS.DATE_TIME_ORIGINAL) {
|
||||||
correctCreationTime = getUNIXTime(
|
correctCreationTime = getUnixTimeInMicroSeconds(
|
||||||
exifData?.DateTimeOriginal
|
exifData?.DateTimeOriginal
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
correctCreationTime = getUNIXTime(exifData?.CreateDate);
|
correctCreationTime = getUnixTimeInMicroSeconds(
|
||||||
|
exifData?.CreateDate
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -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 { Location } from 'types/upload';
|
||||||
import exifr from 'exifr';
|
import exifr from 'exifr';
|
||||||
import piexif from 'piexifjs';
|
import piexif from 'piexifjs';
|
||||||
import { FileTypeInfo } from 'types/upload';
|
import { FileTypeInfo } from 'types/upload';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
|
import { ParsedExtractedMetadata } from 'types/upload';
|
||||||
|
import { getUnixTimeInMicroSeconds } from 'utils/time';
|
||||||
|
|
||||||
const EXIF_TAGS_NEEDED = [
|
const EXIF_TAGS_NEEDED = [
|
||||||
'DateTimeOriginal',
|
'DateTimeOriginal',
|
||||||
|
@ -23,37 +25,29 @@ interface Exif {
|
||||||
GPSLatitudeRef?: number;
|
GPSLatitudeRef?: number;
|
||||||
GPSLongitudeRef?: number;
|
GPSLongitudeRef?: number;
|
||||||
}
|
}
|
||||||
interface ParsedEXIFData {
|
|
||||||
location: Location;
|
|
||||||
creationTime: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getExifData(
|
export async function getExifData(
|
||||||
receivedFile: File,
|
receivedFile: File,
|
||||||
fileTypeInfo: FileTypeInfo
|
fileTypeInfo: FileTypeInfo
|
||||||
): Promise<ParsedEXIFData> {
|
): Promise<ParsedExtractedMetadata> {
|
||||||
const nullExifData: ParsedEXIFData = {
|
let parsedEXIFData = NULL_EXTRACTED_METADATA;
|
||||||
location: NULL_LOCATION,
|
|
||||||
creationTime: null,
|
|
||||||
};
|
|
||||||
try {
|
try {
|
||||||
const exifData = await getRawExif(receivedFile, fileTypeInfo);
|
const exifData = await getRawExif(receivedFile, fileTypeInfo);
|
||||||
if (!exifData) {
|
if (!exifData) {
|
||||||
return nullExifData;
|
return parsedEXIFData;
|
||||||
}
|
}
|
||||||
const parsedEXIFData = {
|
parsedEXIFData = {
|
||||||
location: getEXIFLocation(exifData),
|
location: getEXIFLocation(exifData),
|
||||||
creationTime: getUNIXTime(
|
creationTime: getUnixTimeInMicroSeconds(
|
||||||
exifData.DateTimeOriginal ??
|
exifData.DateTimeOriginal ??
|
||||||
exifData.CreateDate ??
|
exifData.CreateDate ??
|
||||||
exifData.ModifyDate
|
exifData.ModifyDate
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
return parsedEXIFData;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'getExifData failed');
|
logError(e, 'getExifData failed');
|
||||||
return nullExifData;
|
|
||||||
}
|
}
|
||||||
|
return parsedEXIFData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateFileCreationDateInEXIF(
|
export async function updateFileCreationDateInEXIF(
|
||||||
|
@ -131,22 +125,6 @@ export async function getRawExif(
|
||||||
return exifData;
|
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 {
|
function getEXIFLocation(exifData): Location {
|
||||||
if (!exifData.latitude || !exifData.longitude) {
|
if (!exifData.latitude || !exifData.longitude) {
|
||||||
return NULL_LOCATION;
|
return NULL_LOCATION;
|
||||||
|
|
|
@ -6,9 +6,11 @@ import {
|
||||||
ParsedMetadataJSON,
|
ParsedMetadataJSON,
|
||||||
Location,
|
Location,
|
||||||
FileTypeInfo,
|
FileTypeInfo,
|
||||||
|
ParsedExtractedMetadata,
|
||||||
} from 'types/upload';
|
} from 'types/upload';
|
||||||
import { NULL_LOCATION } from 'constants/upload';
|
import { NULL_EXTRACTED_METADATA, NULL_LOCATION } from 'constants/upload';
|
||||||
import { splitFilenameAndExtension } from 'utils/file';
|
import { splitFilenameAndExtension } from 'utils/file';
|
||||||
|
import { getVideoMetadata } from './videoMetadataService';
|
||||||
|
|
||||||
interface ParsedMetadataJSONWithTitle {
|
interface ParsedMetadataJSONWithTitle {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -25,23 +27,25 @@ export async function extractMetadata(
|
||||||
receivedFile: File,
|
receivedFile: File,
|
||||||
fileTypeInfo: FileTypeInfo
|
fileTypeInfo: FileTypeInfo
|
||||||
) {
|
) {
|
||||||
let exifData = null;
|
let extractedMetadata: ParsedExtractedMetadata = NULL_EXTRACTED_METADATA;
|
||||||
if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) {
|
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]}.${
|
title: `${splitFilenameAndExtension(receivedFile.name)[0]}.${
|
||||||
fileTypeInfo.exactType
|
fileTypeInfo.exactType
|
||||||
}`,
|
}`,
|
||||||
creationTime:
|
creationTime:
|
||||||
exifData?.creationTime ?? receivedFile.lastModified * 1000,
|
extractedMetadata.creationTime ?? receivedFile.lastModified * 1000,
|
||||||
modificationTime: receivedFile.lastModified * 1000,
|
modificationTime: receivedFile.lastModified * 1000,
|
||||||
latitude: exifData?.location?.latitude,
|
latitude: extractedMetadata.location.latitude,
|
||||||
longitude: exifData?.location?.longitude,
|
longitude: extractedMetadata.location.longitude,
|
||||||
fileType: fileTypeInfo.fileType,
|
fileType: fileTypeInfo.fileType,
|
||||||
};
|
};
|
||||||
return extractedMetadata;
|
return metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getMetadataJSONMapKey = (
|
export const getMetadataJSONMapKey = (
|
||||||
|
|
|
@ -15,7 +15,7 @@ const MAX_THUMBNAIL_SIZE = 100 * 1024;
|
||||||
const MIN_QUALITY = 0.5;
|
const MIN_QUALITY = 0.5;
|
||||||
const MAX_QUALITY = 0.7;
|
const MAX_QUALITY = 0.7;
|
||||||
|
|
||||||
const WAIT_TIME_THUMBNAIL_GENERATION = 10 * 1000;
|
const WAIT_TIME_THUMBNAIL_GENERATION = 30 * 1000;
|
||||||
|
|
||||||
interface Dimension {
|
interface Dimension {
|
||||||
width: number;
|
width: number;
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { getDedicatedCryptoWorker } from 'utils/crypto';
|
||||||
import {
|
import {
|
||||||
sortFilesIntoCollections,
|
sortFilesIntoCollections,
|
||||||
sortFiles,
|
sortFiles,
|
||||||
removeUnnecessaryFileProps,
|
preservePhotoswipeProps,
|
||||||
} from 'utils/file';
|
} from 'utils/file';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import { getMetadataJSONMapKey, parseMetadataJSON } from './metadataService';
|
import { getMetadataJSONMapKey, parseMetadataJSON } from './metadataService';
|
||||||
|
@ -239,10 +239,8 @@ class UploadManager {
|
||||||
if (fileUploadResult === FileUploadResults.UPLOADED) {
|
if (fileUploadResult === FileUploadResults.UPLOADED) {
|
||||||
this.existingFiles.push(file);
|
this.existingFiles.push(file);
|
||||||
this.existingFiles = sortFiles(this.existingFiles);
|
this.existingFiles = sortFiles(this.existingFiles);
|
||||||
await setLocalFiles(
|
await setLocalFiles(this.existingFiles);
|
||||||
removeUnnecessaryFileProps(this.existingFiles)
|
this.setFiles(preservePhotoswipeProps(this.existingFiles));
|
||||||
);
|
|
||||||
this.setFiles(this.existingFiles);
|
|
||||||
if (!this.existingFilesCollectionWise.has(file.collectionID)) {
|
if (!this.existingFilesCollectionWise.has(file.collectionID)) {
|
||||||
this.existingFilesCollectionWise.set(file.collectionID, []);
|
this.existingFilesCollectionWise.set(file.collectionID, []);
|
||||||
}
|
}
|
||||||
|
|
76
src/services/upload/videoMetadataService.ts
Normal file
76
src/services/upload/videoMetadataService.ts
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -130,3 +130,8 @@ export interface UploadFile extends BackupedFile {
|
||||||
encryptedKey: string;
|
encryptedKey: string;
|
||||||
keyDecryptionNonce: string;
|
keyDecryptionNonce: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ParsedExtractedMetadata {
|
||||||
|
location: Location;
|
||||||
|
creationTime: number;
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,9 @@ import { CustomError } from '../error';
|
||||||
import { logError } from '../sentry';
|
import { logError } from '../sentry';
|
||||||
|
|
||||||
const PAYMENT_PROVIDER_STRIPE = 'stripe';
|
const PAYMENT_PROVIDER_STRIPE = 'stripe';
|
||||||
|
const PAYMENT_PROVIDER_APPSTORE = 'appstore';
|
||||||
|
const PAYMENT_PROVIDER_PLAYSTORE = 'playstore';
|
||||||
|
const PAYMENT_PROVIDER_PAYPAL = 'paypal';
|
||||||
const FREE_PLAN = 'free';
|
const FREE_PLAN = 'free';
|
||||||
|
|
||||||
enum FAILURE_REASON {
|
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(
|
export async function updateSubscription(
|
||||||
plan: Plan,
|
plan: Plan,
|
||||||
setDialogMessage: SetDialogMessage,
|
setDialogMessage: SetDialogMessage,
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
} from 'constants/file';
|
} from 'constants/file';
|
||||||
import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager';
|
import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager';
|
||||||
import HEICConverter from 'services/HEICConverter';
|
import HEICConverter from 'services/HEICConverter';
|
||||||
|
import ffmpegService from 'services/ffmpegService';
|
||||||
|
|
||||||
export function downloadAsFile(filename: string, content: string) {
|
export function downloadAsFile(filename: string, content: string) {
|
||||||
const file = new Blob([content], {
|
const file = new Blob([content], {
|
||||||
|
@ -52,7 +53,7 @@ export async function downloadFile(
|
||||||
if (accessedThroughSharedURL) {
|
if (accessedThroughSharedURL) {
|
||||||
fileURL = await PublicCollectionDownloadManager.getCachedOriginalFile(
|
fileURL = await PublicCollectionDownloadManager.getCachedOriginalFile(
|
||||||
file
|
file
|
||||||
);
|
)[0];
|
||||||
tempURL;
|
tempURL;
|
||||||
if (!fileURL) {
|
if (!fileURL) {
|
||||||
tempURL = URL.createObjectURL(
|
tempURL = URL.createObjectURL(
|
||||||
|
@ -68,7 +69,7 @@ export async function downloadFile(
|
||||||
fileURL = tempURL;
|
fileURL = tempURL;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fileURL = await DownloadManager.getCachedOriginalFile(file);
|
fileURL = await DownloadManager.getCachedOriginalFile(file)[0];
|
||||||
if (!fileURL) {
|
if (!fileURL) {
|
||||||
tempURL = URL.createObjectURL(
|
tempURL = URL.createObjectURL(
|
||||||
await new Response(
|
await new Response(
|
||||||
|
@ -281,20 +282,18 @@ export async function decryptFile(file: EnteFile, collectionKey: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeUnnecessaryFileProps(files: EnteFile[]): EnteFile[] {
|
export const preservePhotoswipeProps =
|
||||||
const stripedFiles = files.map((file) => {
|
(newFiles: EnteFile[]) =>
|
||||||
delete file.src;
|
(currentFiles: EnteFile[]): EnteFile[] => {
|
||||||
delete file.msrc;
|
const currentFilesMap = Object.fromEntries(
|
||||||
delete file.file.objectKey;
|
currentFiles.map((file) => [file.id, file])
|
||||||
delete file.thumbnail.objectKey;
|
);
|
||||||
delete file.h;
|
const fileWithPreservedProperty = newFiles.map((file) => {
|
||||||
delete file.html;
|
const currentFile = currentFilesMap[file.id];
|
||||||
delete file.w;
|
return { ...currentFile, ...file };
|
||||||
|
});
|
||||||
return file;
|
return fileWithPreservedProperty;
|
||||||
});
|
};
|
||||||
return stripedFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fileNameWithoutExtension(filename) {
|
export function fileNameWithoutExtension(filename) {
|
||||||
const lastDotPosition = filename.lastIndexOf('.');
|
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<Blob[]> {
|
||||||
|
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) {
|
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||||
const originalName = fileNameWithoutExtension(file.metadata.title);
|
const originalName = fileNameWithoutExtension(file.metadata.title);
|
||||||
const motionPhoto = await decodeMotionPhoto(fileBlob, originalName);
|
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);
|
fileBlob = await convertIfHEIC(file.metadata.title, fileBlob);
|
||||||
const reader = new FileReader();
|
return [fileBlob];
|
||||||
|
|
||||||
const mimeType =
|
|
||||||
(await getFileTypeFromBlob(reader, fileBlob))?.mime ??
|
|
||||||
typeFromExtension;
|
|
||||||
if (isFileHEIC(mimeType)) {
|
|
||||||
fileBlob = await HEICConverter.convert(fileBlob);
|
|
||||||
}
|
|
||||||
return fileBlob;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fileIsArchived(file: EnteFile) {
|
export function fileIsArchived(file: EnteFile) {
|
||||||
|
|
|
@ -13,3 +13,22 @@ export async function isPlaybackPossible(url: string): Promise<boolean> {
|
||||||
video.src = url;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -104,7 +104,7 @@ const englishConstants = {
|
||||||
UPLOAD: {
|
UPLOAD: {
|
||||||
0: 'preparing to upload',
|
0: 'preparing to upload',
|
||||||
1: 'reading google metadata files',
|
1: 'reading google metadata files',
|
||||||
2: 'reading file metadata to organize file',
|
2: 'reading file metadata',
|
||||||
3: (fileCounter) =>
|
3: (fileCounter) =>
|
||||||
`${fileCounter.finished} / ${fileCounter.total} files backed up`,
|
`${fileCounter.finished} / ${fileCounter.total} files backed up`,
|
||||||
4: 'backup complete',
|
4: 'backup complete',
|
||||||
|
@ -332,6 +332,14 @@ const englishConstants = {
|
||||||
SUBSCRIPTION_PURCHASE_SUCCESS_TITLE: 'thank you',
|
SUBSCRIPTION_PURCHASE_SUCCESS_TITLE: 'thank you',
|
||||||
CANCEL_SUBSCRIPTION_ON_MOBILE:
|
CANCEL_SUBSCRIPTION_ON_MOBILE:
|
||||||
'please cancel your subscription from the mobile app to activate a subscription here',
|
'please cancel your subscription from the mobile app to activate a subscription here',
|
||||||
|
PAYPAL_MANAGE_NOT_SUPPORTED_MESSAGE: () => (
|
||||||
|
<>
|
||||||
|
please contact us at{' '}
|
||||||
|
<a href="mailto:paypal@ente.io">paypal@ente.io</a> to manage your
|
||||||
|
subscription
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
PAYPAL_MANAGE_NOT_SUPPORTED: 'manage paypal plan',
|
||||||
RENAME: 'rename',
|
RENAME: 'rename',
|
||||||
RENAME_COLLECTION: 'rename album',
|
RENAME_COLLECTION: 'rename album',
|
||||||
CONFIRM_DELETE_COLLECTION: 'confirm album deletion',
|
CONFIRM_DELETE_COLLECTION: 'confirm album deletion',
|
||||||
|
@ -674,6 +682,7 @@ const englishConstants = {
|
||||||
ENTE_IO: 'ente.io',
|
ENTE_IO: 'ente.io',
|
||||||
PLAYBACK_SUPPORT_COMING: 'playback support coming soon...',
|
PLAYBACK_SUPPORT_COMING: 'playback support coming soon...',
|
||||||
LIVE_PHOTO: 'this is a live photo',
|
LIVE_PHOTO: 'this is a live photo',
|
||||||
|
LIVE: 'LIVE',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default englishConstants;
|
export default englishConstants;
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
export function getUTCMicroSecondsSinceEpoch(): number {
|
export function getUnixTimeInMicroSeconds(dateTime: Date) {
|
||||||
const now = new Date();
|
if (!dateTime || isNaN(dateTime.getTime())) {
|
||||||
const utcMilllisecondsSinceEpoch =
|
return null;
|
||||||
now.getTime() + now.getTimezoneOffset() * 60 * 1000;
|
}
|
||||||
const utcSecondsSinceEpoch = Math.round(utcMilllisecondsSinceEpoch * 1000);
|
const unixTime = dateTime.getTime() * 1000;
|
||||||
return utcSecondsSinceEpoch;
|
if (unixTime <= 0) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return unixTime;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue