Merge branch 'master' into trash
This commit is contained in:
commit
7ccc0b59f7
220
src/components/FixLargeThumbnail.tsx
Normal file
220
src/components/FixLargeThumbnail.tsx
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
import constants from 'utils/strings/constants';
|
||||||
|
import MessageDialog from './MessageDialog';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { ProgressBar, Button } from 'react-bootstrap';
|
||||||
|
import { ComfySpan } from './ExportInProgress';
|
||||||
|
import {
|
||||||
|
getLargeThumbnailFiles,
|
||||||
|
replaceThumbnail,
|
||||||
|
} from 'services/migrateThumbnailService';
|
||||||
|
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
||||||
|
import { logError } from 'utils/sentry';
|
||||||
|
|
||||||
|
export type SetProgressTracker = React.Dispatch<
|
||||||
|
React.SetStateAction<{
|
||||||
|
current: number;
|
||||||
|
total: number;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
show: () => void;
|
||||||
|
hide: () => void;
|
||||||
|
}
|
||||||
|
export enum FIX_STATE {
|
||||||
|
NOT_STARTED,
|
||||||
|
FIX_LATER,
|
||||||
|
RUNNING,
|
||||||
|
COMPLETED,
|
||||||
|
COMPLETED_WITH_ERRORS,
|
||||||
|
}
|
||||||
|
function Message(props: { fixState: FIX_STATE }) {
|
||||||
|
let message = null;
|
||||||
|
switch (props.fixState) {
|
||||||
|
case FIX_STATE.NOT_STARTED:
|
||||||
|
case FIX_STATE.FIX_LATER:
|
||||||
|
message = constants.REPLACE_THUMBNAIL_NOT_STARTED();
|
||||||
|
break;
|
||||||
|
case FIX_STATE.COMPLETED:
|
||||||
|
message = constants.REPLACE_THUMBNAIL_COMPLETED();
|
||||||
|
break;
|
||||||
|
case FIX_STATE.COMPLETED_WITH_ERRORS:
|
||||||
|
message = constants.REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return message ? (
|
||||||
|
<div style={{ marginBottom: '30px' }}>{message}</div>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default function FixLargeThumbnails(props: Props) {
|
||||||
|
const [fixState, setFixState] = useState(FIX_STATE.NOT_STARTED);
|
||||||
|
const [progressTracker, setProgressTracker] = useState({
|
||||||
|
current: 0,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
const [largeThumbnailFiles, setLargeThumbnailFiles] = useState<number[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const init = (): FIX_STATE => {
|
||||||
|
let fixState = getData(LS_KEYS.THUMBNAIL_FIX_STATE)?.state;
|
||||||
|
if (!fixState || fixState === FIX_STATE.RUNNING) {
|
||||||
|
fixState = FIX_STATE.NOT_STARTED;
|
||||||
|
updateFixState(fixState);
|
||||||
|
}
|
||||||
|
setFixState(fixState);
|
||||||
|
return fixState;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchLargeThumbnail = async () => {
|
||||||
|
const largeThumbnailFiles = (await getLargeThumbnailFiles()) ?? [];
|
||||||
|
setLargeThumbnailFiles(largeThumbnailFiles);
|
||||||
|
return largeThumbnailFiles;
|
||||||
|
};
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
const largeThumbnailFiles = await fetchLargeThumbnail();
|
||||||
|
if (
|
||||||
|
fixState === FIX_STATE.NOT_STARTED &&
|
||||||
|
largeThumbnailFiles.length > 0
|
||||||
|
) {
|
||||||
|
props.show();
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
fixState === FIX_STATE.COMPLETED &&
|
||||||
|
largeThumbnailFiles.length > 0
|
||||||
|
) {
|
||||||
|
updateFixState(FIX_STATE.NOT_STARTED);
|
||||||
|
logError(Error(), 'large thumbnail files left after migration');
|
||||||
|
}
|
||||||
|
if (largeThumbnailFiles.length === 0) {
|
||||||
|
updateFixState(FIX_STATE.COMPLETED);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.isOpen && fixState !== FIX_STATE.RUNNING) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
}, [props.isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fixState = init();
|
||||||
|
if (fixState === FIX_STATE.NOT_STARTED) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
const startFix = async (newlyFetchedLargeThumbnailFiles?: number[]) => {
|
||||||
|
updateFixState(FIX_STATE.RUNNING);
|
||||||
|
const completedWithError = await replaceThumbnail(
|
||||||
|
setProgressTracker,
|
||||||
|
new Set(
|
||||||
|
newlyFetchedLargeThumbnailFiles ?? largeThumbnailFiles ?? []
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (typeof completedWithError !== 'undefined') {
|
||||||
|
updateFixState(
|
||||||
|
completedWithError
|
||||||
|
? FIX_STATE.COMPLETED_WITH_ERRORS
|
||||||
|
: FIX_STATE.COMPLETED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await fetchLargeThumbnail();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFixState = (fixState: FIX_STATE) => {
|
||||||
|
setFixState(fixState);
|
||||||
|
setData(LS_KEYS.THUMBNAIL_FIX_STATE, { state: fixState });
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<MessageDialog
|
||||||
|
show={props.isOpen}
|
||||||
|
onHide={props.hide}
|
||||||
|
attributes={{
|
||||||
|
title: constants.FIX_LARGE_THUMBNAILS,
|
||||||
|
staticBackdrop: true,
|
||||||
|
}}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginBottom: '20px',
|
||||||
|
padding: '0 5%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}>
|
||||||
|
<Message fixState={fixState} />
|
||||||
|
|
||||||
|
{fixState === FIX_STATE.RUNNING && (
|
||||||
|
<>
|
||||||
|
<div style={{ marginBottom: '10px' }}>
|
||||||
|
<ComfySpan>
|
||||||
|
{' '}
|
||||||
|
{progressTracker.current} /{' '}
|
||||||
|
{progressTracker.total}{' '}
|
||||||
|
</ComfySpan>{' '}
|
||||||
|
<span style={{ marginLeft: '10px' }}>
|
||||||
|
{' '}
|
||||||
|
{constants.THUMBNAIL_REPLACED}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
marginTop: '10px',
|
||||||
|
marginBottom: '20px',
|
||||||
|
}}>
|
||||||
|
<ProgressBar
|
||||||
|
now={Math.round(
|
||||||
|
(progressTracker.current * 100) /
|
||||||
|
progressTracker.total
|
||||||
|
)}
|
||||||
|
animated={true}
|
||||||
|
variant="upload-progress-bar"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-around',
|
||||||
|
}}>
|
||||||
|
{fixState === FIX_STATE.NOT_STARTED ? (
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
variant={'outline-secondary'}
|
||||||
|
onClick={() => {
|
||||||
|
updateFixState(FIX_STATE.FIX_LATER);
|
||||||
|
props.hide();
|
||||||
|
}}>
|
||||||
|
{constants.FIX_LATER}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
variant={'outline-secondary'}
|
||||||
|
onClick={props.hide}>
|
||||||
|
{constants.CLOSE}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{(fixState === FIX_STATE.NOT_STARTED ||
|
||||||
|
fixState === FIX_STATE.FIX_LATER ||
|
||||||
|
fixState === FIX_STATE.COMPLETED_WITH_ERRORS) && (
|
||||||
|
<>
|
||||||
|
<div style={{ width: '30px' }} />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
variant={'outline-success'}
|
||||||
|
onClick={() => startFix()}>
|
||||||
|
{constants.FIX}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MessageDialog>
|
||||||
|
);
|
||||||
|
}
|
|
@ -18,7 +18,6 @@ import { VariableSizeList as List } from 'react-window';
|
||||||
import PhotoSwipe from 'components/PhotoSwipe/PhotoSwipe';
|
import PhotoSwipe from 'components/PhotoSwipe/PhotoSwipe';
|
||||||
import { isInsideBox, isSameDay as isSameDayAnyYear } from 'utils/search';
|
import { isInsideBox, isSameDay as isSameDayAnyYear } from 'utils/search';
|
||||||
import { SetDialogMessage } from './MessageDialog';
|
import { SetDialogMessage } from './MessageDialog';
|
||||||
import { CustomError } from 'utils/common/errorUtil';
|
|
||||||
import {
|
import {
|
||||||
GAP_BTW_TILES,
|
GAP_BTW_TILES,
|
||||||
DATE_CONTAINER_HEIGHT,
|
DATE_CONTAINER_HEIGHT,
|
||||||
|
@ -34,10 +33,10 @@ import {
|
||||||
TRASH_SECTION,
|
TRASH_SECTION,
|
||||||
} from './pages/gallery/Collections';
|
} from './pages/gallery/Collections';
|
||||||
import { isSharedFile } from 'utils/file';
|
import { isSharedFile } from 'utils/file';
|
||||||
|
import { isPlaybackPossible } from 'utils/photoFrame';
|
||||||
|
|
||||||
const NO_OF_PAGES = 2;
|
const NO_OF_PAGES = 2;
|
||||||
const A_DAY = 24 * 60 * 60 * 1000;
|
const A_DAY = 24 * 60 * 60 * 1000;
|
||||||
const WAIT_FOR_VIDEO_PLAYBACK = 1 * 1000;
|
|
||||||
|
|
||||||
interface TimeStampListItem {
|
interface TimeStampListItem {
|
||||||
itemType: ITEM_TYPE;
|
itemType: ITEM_TYPE;
|
||||||
|
@ -150,7 +149,7 @@ interface Props {
|
||||||
isFirstLoad;
|
isFirstLoad;
|
||||||
openFileUploader;
|
openFileUploader;
|
||||||
loadingBar;
|
loadingBar;
|
||||||
searchMode: boolean;
|
isInSearchMode: boolean;
|
||||||
search: Search;
|
search: Search;
|
||||||
setSearchStats: setSearchStats;
|
setSearchStats: setSearchStats;
|
||||||
deleted?: number[];
|
deleted?: number[];
|
||||||
|
@ -169,11 +168,10 @@ const PhotoFrame = ({
|
||||||
isFirstLoad,
|
isFirstLoad,
|
||||||
openFileUploader,
|
openFileUploader,
|
||||||
loadingBar,
|
loadingBar,
|
||||||
searchMode,
|
isInSearchMode,
|
||||||
search,
|
search,
|
||||||
setSearchStats,
|
setSearchStats,
|
||||||
deleted,
|
deleted,
|
||||||
setDialogMessage,
|
|
||||||
activeCollection,
|
activeCollection,
|
||||||
isSharedCollection,
|
isSharedCollection,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
@ -185,12 +183,20 @@ const PhotoFrame = ({
|
||||||
const listRef = useRef(null);
|
const listRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchMode) {
|
if (isInSearchMode) {
|
||||||
setSearchStats({
|
setSearchStats({
|
||||||
resultCount: filteredData.length,
|
resultCount: filteredData.length,
|
||||||
timeTaken: (Date.now() - startTime) / 1000,
|
timeTaken: (Date.now() - startTime) / 1000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (search.fileIndex || search.fileIndex === 0) {
|
||||||
|
const filteredDataIdx = filteredData.findIndex(
|
||||||
|
(data) => data.dataIndex === search.fileIndex
|
||||||
|
);
|
||||||
|
if (filteredDataIdx || filteredDataIdx === 0) {
|
||||||
|
onThumbnailClick(filteredDataIdx)();
|
||||||
|
}
|
||||||
|
}
|
||||||
}, [search]);
|
}, [search]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -229,21 +235,33 @@ const PhotoFrame = ({
|
||||||
setFiles(files);
|
setFiles(files);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSrcUrl = (index: number, url: string) => {
|
const updateSrcUrl = async (index: number, url: string) => {
|
||||||
files[index] = {
|
files[index] = {
|
||||||
...files[index],
|
...files[index],
|
||||||
src: url,
|
|
||||||
w: window.innerWidth,
|
w: window.innerWidth,
|
||||||
h: window.innerHeight,
|
h: window.innerHeight,
|
||||||
};
|
};
|
||||||
if (files[index].metadata.fileType === FILE_TYPE.VIDEO) {
|
if (files[index].metadata.fileType === FILE_TYPE.VIDEO) {
|
||||||
|
if (await isPlaybackPossible(url)) {
|
||||||
files[index].html = `
|
files[index].html = `
|
||||||
<video controls>
|
<video controls>
|
||||||
<source src="${url}" />
|
<source src="${url}" />
|
||||||
Your browser does not support the video tag.
|
Your browser does not support the video tag.
|
||||||
</video>
|
</video>
|
||||||
`;
|
`;
|
||||||
delete files[index].src;
|
} else {
|
||||||
|
files[index].html = `
|
||||||
|
<div class="video-loading">
|
||||||
|
<img src="${files[index].msrc}" />
|
||||||
|
<div class="download-message" >
|
||||||
|
${constants.VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD}
|
||||||
|
<a class="btn btn-outline-success" href=${url} download="${files[index].metadata.title}"">Download</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
files[index].src = url;
|
||||||
}
|
}
|
||||||
setFiles(files);
|
setFiles(files);
|
||||||
};
|
};
|
||||||
|
@ -287,6 +305,7 @@ const PhotoFrame = ({
|
||||||
|
|
||||||
const getSlideData = async (instance: any, index: number, item: File) => {
|
const getSlideData = async (instance: any, index: number, item: File) => {
|
||||||
if (!item.msrc) {
|
if (!item.msrc) {
|
||||||
|
try {
|
||||||
let url: string;
|
let url: string;
|
||||||
if (galleryContext.thumbs.has(item.id)) {
|
if (galleryContext.thumbs.has(item.id)) {
|
||||||
url = galleryContext.thumbs.get(item.id);
|
url = galleryContext.thumbs.get(item.id);
|
||||||
|
@ -307,8 +326,12 @@ const PhotoFrame = ({
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!fetching[item.dataIndex]) {
|
if (!fetching[item.dataIndex]) {
|
||||||
|
try {
|
||||||
fetching[item.dataIndex] = true;
|
fetching[item.dataIndex] = true;
|
||||||
let url: string;
|
let url: string;
|
||||||
if (galleryContext.files.has(item.id)) {
|
if (galleryContext.files.has(item.id)) {
|
||||||
|
@ -317,72 +340,22 @@ const PhotoFrame = ({
|
||||||
url = await DownloadManager.getFile(item, true);
|
url = await DownloadManager.getFile(item, true);
|
||||||
galleryContext.files.set(item.id, url);
|
galleryContext.files.set(item.id, url);
|
||||||
}
|
}
|
||||||
updateSrcUrl(item.dataIndex, url);
|
await updateSrcUrl(item.dataIndex, url);
|
||||||
if (item.metadata.fileType === FILE_TYPE.VIDEO) {
|
item.html = files[item.dataIndex].html;
|
||||||
try {
|
item.src = files[item.dataIndex].src;
|
||||||
await new Promise((resolve, reject) => {
|
item.w = files[item.dataIndex].w;
|
||||||
const video = document.createElement('video');
|
item.h = files[item.dataIndex].h;
|
||||||
video.addEventListener('timeupdate', function () {
|
|
||||||
clearTimeout(t);
|
|
||||||
resolve(null);
|
|
||||||
});
|
|
||||||
video.preload = 'metadata';
|
|
||||||
video.src = url;
|
|
||||||
video.currentTime = 3;
|
|
||||||
const t = setTimeout(() => {
|
|
||||||
reject(
|
|
||||||
Error(
|
|
||||||
`${CustomError.VIDEO_PLAYBACK_FAILED} err: wait time exceeded`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}, WAIT_FOR_VIDEO_PLAYBACK);
|
|
||||||
});
|
|
||||||
item.html = `
|
|
||||||
<video width="320" height="240" controls>
|
|
||||||
<source src="${url}" />
|
|
||||||
Your browser does not support the video tag.
|
|
||||||
</video>
|
|
||||||
`;
|
|
||||||
delete item.src;
|
|
||||||
} catch (e) {
|
|
||||||
const downloadFile = async () => {
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.style.display = 'none';
|
|
||||||
a.href = url;
|
|
||||||
a.download = item.metadata.title;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
a.remove();
|
|
||||||
setOpen(false);
|
|
||||||
};
|
|
||||||
setDialogMessage({
|
|
||||||
title: constants.VIDEO_PLAYBACK_FAILED,
|
|
||||||
content:
|
|
||||||
constants.VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD,
|
|
||||||
staticBackdrop: true,
|
|
||||||
proceed: {
|
|
||||||
text: constants.DOWNLOAD,
|
|
||||||
action: downloadFile,
|
|
||||||
variant: 'success',
|
|
||||||
},
|
|
||||||
close: {
|
|
||||||
text: constants.CLOSE,
|
|
||||||
action: () => setOpen(false),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
item.src = url;
|
|
||||||
}
|
|
||||||
item.w = window.innerWidth;
|
|
||||||
item.h = window.innerHeight;
|
|
||||||
try {
|
try {
|
||||||
instance.invalidateCurrItems();
|
instance.invalidateCurrItems();
|
||||||
instance.updateSize(true);
|
instance.updateSize(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// no-op
|
||||||
|
} finally {
|
||||||
|
fetching[item.dataIndex] = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -531,7 +504,7 @@ const PhotoFrame = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!isFirstLoad && files.length === 0 && !searchMode ? (
|
{!isFirstLoad && files.length === 0 && !isInSearchMode ? (
|
||||||
<EmptyScreen>
|
<EmptyScreen>
|
||||||
<img height={150} src="/images/gallery.png" />
|
<img height={150} src="/images/gallery.png" />
|
||||||
<div style={{ color: '#a6a6a6', marginTop: '16px' }}>
|
<div style={{ color: '#a6a6a6', marginTop: '16px' }}>
|
||||||
|
@ -651,7 +624,7 @@ const PhotoFrame = ({
|
||||||
return sum;
|
return sum;
|
||||||
})();
|
})();
|
||||||
files.length < 30 &&
|
files.length < 30 &&
|
||||||
!searchMode &&
|
!isInSearchMode &&
|
||||||
timeStampList.push({
|
timeStampList.push({
|
||||||
itemType: ITEM_TYPE.BANNER,
|
itemType: ITEM_TYPE.BANNER,
|
||||||
banner: (
|
banner: (
|
||||||
|
|
|
@ -7,16 +7,15 @@ import {
|
||||||
addToFavorites,
|
addToFavorites,
|
||||||
removeFromFavorites,
|
removeFromFavorites,
|
||||||
} from 'services/collectionService';
|
} from 'services/collectionService';
|
||||||
import { File, FILE_TYPE } from 'services/fileService';
|
import { File } from 'services/fileService';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import DownloadManger from 'services/downloadManager';
|
|
||||||
import exifr from 'exifr';
|
import exifr from 'exifr';
|
||||||
import Modal from 'react-bootstrap/Modal';
|
import Modal from 'react-bootstrap/Modal';
|
||||||
import Button from 'react-bootstrap/Button';
|
import Button from 'react-bootstrap/Button';
|
||||||
import Form from 'react-bootstrap/Form';
|
import Form from 'react-bootstrap/Form';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import events from './events';
|
import events from './events';
|
||||||
import { fileNameWithoutExtension, formatDateTime } from 'utils/file';
|
import { downloadFile, formatDateTime } from 'utils/file';
|
||||||
import { FormCheck } from 'react-bootstrap';
|
import { FormCheck } from 'react-bootstrap';
|
||||||
import { prettyPrintExif } from 'utils/exif';
|
import { prettyPrintExif } from 'utils/exif';
|
||||||
|
|
||||||
|
@ -297,21 +296,11 @@ function PhotoSwipe(props: Iprops) {
|
||||||
setShowInfo(true);
|
setShowInfo(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadFile = async (file) => {
|
const downloadFileHelper = async (file) => {
|
||||||
const { loadingBar } = props;
|
const { loadingBar } = props;
|
||||||
const a = document.createElement('a');
|
|
||||||
a.style.display = 'none';
|
|
||||||
loadingBar.current.continuousStart();
|
loadingBar.current.continuousStart();
|
||||||
a.href = await DownloadManger.getFile(file);
|
await downloadFile(file);
|
||||||
loadingBar.current.complete();
|
loadingBar.current.complete();
|
||||||
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
|
||||||
a.download = fileNameWithoutExtension(file.metadata.title) + '.zip';
|
|
||||||
} else {
|
|
||||||
a.download = file.metadata.title;
|
|
||||||
}
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
a.remove();
|
|
||||||
};
|
};
|
||||||
const { id } = props;
|
const { id } = props;
|
||||||
let { className } = props;
|
let { className } = props;
|
||||||
|
@ -345,7 +334,7 @@ function PhotoSwipe(props: Iprops) {
|
||||||
className="pswp-custom download-btn"
|
className="pswp-custom download-btn"
|
||||||
title={constants.DOWNLOAD}
|
title={constants.DOWNLOAD}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
downloadFile(photoSwipe.currItem)
|
downloadFileHelper(photoSwipe.currItem)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
getYearSuggestion,
|
getYearSuggestion,
|
||||||
parseHumanDate,
|
parseHumanDate,
|
||||||
searchCollection,
|
searchCollection,
|
||||||
|
searchFiles,
|
||||||
searchLocation,
|
searchLocation,
|
||||||
} from 'services/searchService';
|
} from 'services/searchService';
|
||||||
import { getFormattedDate } from 'utils/search';
|
import { getFormattedDate } from 'utils/search';
|
||||||
|
@ -20,6 +21,9 @@ import SearchIcon from './icons/SearchIcon';
|
||||||
import CrossIcon from './icons/CrossIcon';
|
import CrossIcon from './icons/CrossIcon';
|
||||||
import { Collection } from 'services/collectionService';
|
import { Collection } from 'services/collectionService';
|
||||||
import CollectionIcon from './icons/CollectionIcon';
|
import CollectionIcon from './icons/CollectionIcon';
|
||||||
|
import { File, FILE_TYPE } from 'services/fileService';
|
||||||
|
import ImageIcon from './icons/ImageIcon';
|
||||||
|
import VideoIcon from './icons/VideoIcon';
|
||||||
|
|
||||||
const Wrapper = styled.div<{ isDisabled: boolean; isOpen: boolean }>`
|
const Wrapper = styled.div<{ isDisabled: boolean; isOpen: boolean }>`
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
@ -74,6 +78,8 @@ export enum SuggestionType {
|
||||||
DATE,
|
DATE,
|
||||||
LOCATION,
|
LOCATION,
|
||||||
COLLECTION,
|
COLLECTION,
|
||||||
|
IMAGE,
|
||||||
|
VIDEO,
|
||||||
}
|
}
|
||||||
export interface DateValue {
|
export interface DateValue {
|
||||||
date?: number;
|
date?: number;
|
||||||
|
@ -94,6 +100,7 @@ interface Props {
|
||||||
searchStats: SearchStats;
|
searchStats: SearchStats;
|
||||||
collections: Collection[];
|
collections: Collection[];
|
||||||
setActiveCollection: (id: number) => void;
|
setActiveCollection: (id: number) => void;
|
||||||
|
files: File[];
|
||||||
}
|
}
|
||||||
export default function SearchBar(props: Props) {
|
export default function SearchBar(props: Props) {
|
||||||
const [value, setValue] = useState<Suggestion>(null);
|
const [value, setValue] = useState<Suggestion>(null);
|
||||||
|
@ -112,14 +119,14 @@ export default function SearchBar(props: Props) {
|
||||||
if (!searchPhrase?.length) {
|
if (!searchPhrase?.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const option = [
|
const options = [
|
||||||
...getHolidaySuggestion(searchPhrase),
|
...getHolidaySuggestion(searchPhrase),
|
||||||
...getYearSuggestion(searchPhrase),
|
...getYearSuggestion(searchPhrase),
|
||||||
];
|
];
|
||||||
|
|
||||||
const searchedDates = parseHumanDate(searchPhrase);
|
const searchedDates = parseHumanDate(searchPhrase);
|
||||||
|
|
||||||
option.push(
|
options.push(
|
||||||
...searchedDates.map((searchedDate) => ({
|
...searchedDates.map((searchedDate) => ({
|
||||||
type: SuggestionType.DATE,
|
type: SuggestionType.DATE,
|
||||||
value: searchedDate,
|
value: searchedDate,
|
||||||
|
@ -131,7 +138,7 @@ export default function SearchBar(props: Props) {
|
||||||
searchPhrase,
|
searchPhrase,
|
||||||
props.collections
|
props.collections
|
||||||
);
|
);
|
||||||
option.push(
|
options.push(
|
||||||
...collectionResults.map(
|
...collectionResults.map(
|
||||||
(searchResult) =>
|
(searchResult) =>
|
||||||
({
|
({
|
||||||
|
@ -141,8 +148,20 @@ export default function SearchBar(props: Props) {
|
||||||
} as Suggestion)
|
} as Suggestion)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
const fileResults = searchFiles(searchPhrase, props.files);
|
||||||
|
options.push(
|
||||||
|
...fileResults.map((file) => ({
|
||||||
|
type:
|
||||||
|
file.type === FILE_TYPE.IMAGE
|
||||||
|
? SuggestionType.IMAGE
|
||||||
|
: SuggestionType.VIDEO,
|
||||||
|
value: file.index,
|
||||||
|
label: file.title,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
const locationResults = await searchLocation(searchPhrase);
|
const locationResults = await searchLocation(searchPhrase);
|
||||||
option.push(
|
options.push(
|
||||||
...locationResults.map(
|
...locationResults.map(
|
||||||
(searchResult) =>
|
(searchResult) =>
|
||||||
({
|
({
|
||||||
|
@ -152,7 +171,7 @@ export default function SearchBar(props: Props) {
|
||||||
} as Suggestion)
|
} as Suggestion)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
return option;
|
return options;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOptions = debounce(getAutoCompleteSuggestions, 250);
|
const getOptions = debounce(getAutoCompleteSuggestions, 250);
|
||||||
|
@ -161,7 +180,6 @@ export default function SearchBar(props: Props) {
|
||||||
if (!selectedOption) {
|
if (!selectedOption) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (selectedOption.type) {
|
switch (selectedOption.type) {
|
||||||
case SuggestionType.DATE:
|
case SuggestionType.DATE:
|
||||||
props.setSearch({
|
props.setSearch({
|
||||||
|
@ -177,12 +195,17 @@ export default function SearchBar(props: Props) {
|
||||||
break;
|
break;
|
||||||
case SuggestionType.COLLECTION:
|
case SuggestionType.COLLECTION:
|
||||||
props.setActiveCollection(selectedOption.value as number);
|
props.setActiveCollection(selectedOption.value as number);
|
||||||
resetSearch(true);
|
setValue(null);
|
||||||
|
break;
|
||||||
|
case SuggestionType.IMAGE:
|
||||||
|
case SuggestionType.VIDEO:
|
||||||
|
props.setSearch({ fileIndex: selectedOption.value as number });
|
||||||
|
setValue(null);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const resetSearch = async (force?: boolean) => {
|
const resetSearch = () => {
|
||||||
if (props.isOpen || force) {
|
if (props.isOpen) {
|
||||||
props.loadingBar.current?.continuousStart();
|
props.loadingBar.current?.continuousStart();
|
||||||
props.setSearch({});
|
props.setSearch({});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -205,6 +228,10 @@ export default function SearchBar(props: Props) {
|
||||||
return <LocationIcon />;
|
return <LocationIcon />;
|
||||||
case SuggestionType.COLLECTION:
|
case SuggestionType.COLLECTION:
|
||||||
return <CollectionIcon />;
|
return <CollectionIcon />;
|
||||||
|
case SuggestionType.IMAGE:
|
||||||
|
return <ImageIcon />;
|
||||||
|
case SuggestionType.VIDEO:
|
||||||
|
return <VideoIcon />;
|
||||||
default:
|
default:
|
||||||
return <SearchIcon />;
|
return <SearchIcon />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ import {
|
||||||
ARCHIVE_SECTION,
|
ARCHIVE_SECTION,
|
||||||
TRASH_SECTION,
|
TRASH_SECTION,
|
||||||
} from 'components/pages/gallery/Collections';
|
} from 'components/pages/gallery/Collections';
|
||||||
|
import FixLargeThumbnails from './FixLargeThumbnail';
|
||||||
interface Props {
|
interface Props {
|
||||||
collections: Collection[];
|
collections: Collection[];
|
||||||
setDialogMessage: SetDialogMessage;
|
setDialogMessage: SetDialogMessage;
|
||||||
|
@ -53,6 +54,7 @@ export default function Sidebar(props: Props) {
|
||||||
const [recoverModalView, setRecoveryModalView] = useState(false);
|
const [recoverModalView, setRecoveryModalView] = useState(false);
|
||||||
const [twoFactorModalView, setTwoFactorModalView] = useState(false);
|
const [twoFactorModalView, setTwoFactorModalView] = useState(false);
|
||||||
const [exportModalView, setExportModalView] = useState(false);
|
const [exportModalView, setExportModalView] = useState(false);
|
||||||
|
const [fixLargeThumbsView, setFixLargeThumbsView] = useState(false);
|
||||||
const galleryContext = useContext(GalleryContext);
|
const galleryContext = useContext(GalleryContext);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
|
@ -278,6 +280,18 @@ export default function Sidebar(props: Props) {
|
||||||
{constants.UPDATE_EMAIL}
|
{constants.UPDATE_EMAIL}
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
<>
|
||||||
|
<FixLargeThumbnails
|
||||||
|
isOpen={fixLargeThumbsView}
|
||||||
|
hide={() => setFixLargeThumbsView(false)}
|
||||||
|
show={() => setFixLargeThumbsView(true)}
|
||||||
|
/>
|
||||||
|
<LinkButton
|
||||||
|
style={{ marginTop: '30px' }}
|
||||||
|
onClick={() => setFixLargeThumbsView(true)}>
|
||||||
|
{constants.FIX_LARGE_THUMBNAILS}
|
||||||
|
</LinkButton>
|
||||||
|
</>
|
||||||
<LinkButton
|
<LinkButton
|
||||||
style={{ marginTop: '30px' }}
|
style={{ marginTop: '30px' }}
|
||||||
onClick={openFeedbackURL}>
|
onClick={openFeedbackURL}>
|
||||||
|
|
21
src/components/icons/ImageIcon.tsx
Normal file
21
src/components/icons/ImageIcon.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function ImageIcon(props) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
height={props.height}
|
||||||
|
viewBox={props.viewBox}
|
||||||
|
width={props.width}
|
||||||
|
fill="currentColor">
|
||||||
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||||
|
<path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-4.86 8.86l-3 3.87L9 13.14 6 17h12l-3.86-5.14z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageIcon.defaultProps = {
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
};
|
20
src/components/icons/VideoIcon.tsx
Normal file
20
src/components/icons/VideoIcon.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function VideoIcon(props) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
height={props.height}
|
||||||
|
viewBox={props.viewBox}
|
||||||
|
width={props.width}
|
||||||
|
fill="currentColor">
|
||||||
|
<path d="M4 6.47L5.76 10H20v8H4V6.47M22 4h-4l2 4h-3l-2-4h-2l2 4h-3l-2-4H8l2 4H7L5 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V4z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoIcon.defaultProps = {
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
};
|
|
@ -36,7 +36,7 @@ interface CollectionProps {
|
||||||
syncWithRemote: () => Promise<void>;
|
syncWithRemote: () => Promise<void>;
|
||||||
setCollectionNamerAttributes: SetCollectionNamerAttributes;
|
setCollectionNamerAttributes: SetCollectionNamerAttributes;
|
||||||
startLoadingBar: () => void;
|
startLoadingBar: () => void;
|
||||||
searchMode: boolean;
|
isInSearchMode: boolean;
|
||||||
collectionFilesCount: Map<number, number>;
|
collectionFilesCount: Map<number, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,6 +104,11 @@ const SectionChipCreater =
|
||||||
/>
|
/>
|
||||||
</Chip>
|
</Chip>
|
||||||
);
|
);
|
||||||
|
const Hider = styled.div<{ hide: boolean }>`
|
||||||
|
opacity: ${(props) => (props.hide ? '0' : '100')};
|
||||||
|
height: ${(props) => (props.hide ? '0' : 'auto')};
|
||||||
|
`;
|
||||||
|
|
||||||
export default function Collections(props: CollectionProps) {
|
export default function Collections(props: CollectionProps) {
|
||||||
const { activeCollection, collections, setActiveCollection } = props;
|
const { activeCollection, collections, setActiveCollection } = props;
|
||||||
const [selectedCollectionID, setSelectedCollectionID] =
|
const [selectedCollectionID, setSelectedCollectionID] =
|
||||||
|
@ -136,7 +141,7 @@ export default function Collections(props: CollectionProps) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateScrollObj();
|
updateScrollObj();
|
||||||
}, [collectionWrapperRef.current]);
|
}, [collectionWrapperRef.current, props.isInSearchMode, collections]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!collectionWrapperRef?.current) {
|
if (!collectionWrapperRef?.current) {
|
||||||
|
@ -199,8 +204,7 @@ export default function Collections(props: CollectionProps) {
|
||||||
const SectionChip = SectionChipCreater({ activeCollection, clickHandler });
|
const SectionChip = SectionChipCreater({ activeCollection, clickHandler });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
!props.searchMode && (
|
<Hider hide={props.isInSearchMode}>
|
||||||
<>
|
|
||||||
<CollectionShare
|
<CollectionShare
|
||||||
show={collectionShareModalView}
|
show={collectionShareModalView}
|
||||||
onHide={() => setCollectionShareModalView(false)}
|
onHide={() => setCollectionShareModalView(false)}
|
||||||
|
@ -215,9 +219,7 @@ export default function Collections(props: CollectionProps) {
|
||||||
{scrollObj.scrollLeft > 0 && (
|
{scrollObj.scrollLeft > 0 && (
|
||||||
<NavigationButton
|
<NavigationButton
|
||||||
scrollDirection={SCROLL_DIRECTION.LEFT}
|
scrollDirection={SCROLL_DIRECTION.LEFT}
|
||||||
onClick={scrollCollection(
|
onClick={scrollCollection(SCROLL_DIRECTION.LEFT)}
|
||||||
SCROLL_DIRECTION.LEFT
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Wrapper
|
<Wrapper
|
||||||
|
@ -242,8 +244,7 @@ export default function Collections(props: CollectionProps) {
|
||||||
active={activeCollection === item.id}
|
active={activeCollection === item.id}
|
||||||
onClick={clickHandler(item.id)}>
|
onClick={clickHandler(item.id)}>
|
||||||
{item.name}
|
{item.name}
|
||||||
{item.type !==
|
{item.type !== CollectionType.favorites &&
|
||||||
CollectionType.favorites &&
|
|
||||||
item.owner.id === user?.id ? (
|
item.owner.id === user?.id ? (
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
rootClose
|
rootClose
|
||||||
|
@ -282,9 +283,7 @@ export default function Collections(props: CollectionProps) {
|
||||||
scrollObj.scrollWidth - scrollObj.clientWidth && (
|
scrollObj.scrollWidth - scrollObj.clientWidth && (
|
||||||
<NavigationButton
|
<NavigationButton
|
||||||
scrollDirection={SCROLL_DIRECTION.RIGHT}
|
scrollDirection={SCROLL_DIRECTION.RIGHT}
|
||||||
onClick={scrollCollection(
|
onClick={scrollCollection(SCROLL_DIRECTION.RIGHT)}
|
||||||
SCROLL_DIRECTION.RIGHT
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CollectionContainer>
|
</CollectionContainer>
|
||||||
|
@ -293,7 +292,6 @@ export default function Collections(props: CollectionProps) {
|
||||||
activeSortBy={collectionSortBy}
|
activeSortBy={collectionSortBy}
|
||||||
/>
|
/>
|
||||||
</CollectionBar>
|
</CollectionBar>
|
||||||
</>
|
</Hider>
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,6 +128,7 @@ export default function PreviewCard(props: IProps) {
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (file && !file.msrc) {
|
if (file && !file.msrc) {
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
|
try {
|
||||||
const url = await DownloadManager.getPreview(file);
|
const url = await DownloadManager.getPreview(file);
|
||||||
if (isMounted.current) {
|
if (isMounted.current) {
|
||||||
setImgSrc(url);
|
setImgSrc(url);
|
||||||
|
@ -138,6 +139,9 @@ export default function PreviewCard(props: IProps) {
|
||||||
}
|
}
|
||||||
updateUrl(url);
|
updateUrl(url);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (thumbs.has(file.id)) {
|
if (thumbs.has(file.id)) {
|
||||||
|
|
|
@ -79,12 +79,33 @@ const GlobalStyles = createGlobalStyle`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-loading > div {
|
.video-loading > div.spinner-border {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -50vh;
|
top: -50vh;
|
||||||
left: 50vw;
|
left: 50vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.video-loading > div.download-message {
|
||||||
|
position: relative;
|
||||||
|
top: -60vh;
|
||||||
|
left: 0;
|
||||||
|
height: 16vh;
|
||||||
|
padding:2vh 0;
|
||||||
|
background-color: #151414;
|
||||||
|
color:#ddd;
|
||||||
|
display: flex;
|
||||||
|
flex-direction:column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
opacity: 0.8;
|
||||||
|
font-size:20px;
|
||||||
|
}
|
||||||
|
.download-message > a{
|
||||||
|
width: 130px;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--primary: #e26f99,
|
--primary: #e26f99,
|
||||||
};
|
};
|
||||||
|
|
|
@ -120,6 +120,7 @@ export type setSearchStats = React.Dispatch<React.SetStateAction<SearchStats>>;
|
||||||
export type Search = {
|
export type Search = {
|
||||||
date?: DateValue;
|
date?: DateValue;
|
||||||
location?: Bbox;
|
location?: Bbox;
|
||||||
|
fileIndex?: number;
|
||||||
};
|
};
|
||||||
export interface SearchStats {
|
export interface SearchStats {
|
||||||
resultCount: number;
|
resultCount: number;
|
||||||
|
@ -173,6 +174,7 @@ export default function Gallery() {
|
||||||
const [search, setSearch] = useState<Search>({
|
const [search, setSearch] = useState<Search>({
|
||||||
date: null,
|
date: null,
|
||||||
location: null,
|
location: null,
|
||||||
|
fileIndex: null,
|
||||||
});
|
});
|
||||||
const [uploadInProgress, setUploadInProgress] = useState(false);
|
const [uploadInProgress, setUploadInProgress] = useState(false);
|
||||||
const {
|
const {
|
||||||
|
@ -188,7 +190,7 @@ export default function Gallery() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const loadingBar = useRef(null);
|
const loadingBar = useRef(null);
|
||||||
const [searchMode, setSearchMode] = useState(false);
|
const [isInSearchMode, setIsInSearchMode] = useState(false);
|
||||||
const [searchStats, setSearchStats] = useState(null);
|
const [searchStats, setSearchStats] = useState(null);
|
||||||
const syncInProgress = useRef(true);
|
const syncInProgress = useRef(true);
|
||||||
const resync = useRef(false);
|
const resync = useRef(false);
|
||||||
|
@ -197,11 +199,6 @@ export default function Gallery() {
|
||||||
const [collectionFilesCount, setCollectionFilesCount] =
|
const [collectionFilesCount, setCollectionFilesCount] =
|
||||||
useState<Map<number, number>>();
|
useState<Map<number, number>>();
|
||||||
const [activeCollection, setActiveCollection] = useState<number>(undefined);
|
const [activeCollection, setActiveCollection] = useState<number>(undefined);
|
||||||
|
|
||||||
const [isSharedCollectionActive, setIsSharedCollectionActive] =
|
|
||||||
useState(false);
|
|
||||||
|
|
||||||
const [isFavCollectionActive, setIsFavCollectionActive] = useState(false);
|
|
||||||
const [trash, setTrash] = useState<Trash>([]);
|
const [trash, setTrash] = useState<Trash>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -267,14 +264,6 @@ export default function Gallery() {
|
||||||
}
|
}
|
||||||
const href = `/gallery${collectionURL}`;
|
const href = `/gallery${collectionURL}`;
|
||||||
router.push(href, undefined, { shallow: true });
|
router.push(href, undefined, { shallow: true });
|
||||||
|
|
||||||
setIsSharedCollectionActive(
|
|
||||||
isSharedCollection(activeCollection, collections)
|
|
||||||
);
|
|
||||||
|
|
||||||
setIsFavCollectionActive(
|
|
||||||
isFavoriteCollection(activeCollection, collections)
|
|
||||||
);
|
|
||||||
}, [activeCollection]);
|
}, [activeCollection]);
|
||||||
|
|
||||||
const syncWithRemote = async (force = false, silent = false) => {
|
const syncWithRemote = async (force = false, silent = false) => {
|
||||||
|
@ -483,8 +472,9 @@ export default function Gallery() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSearch = (search: Search) => {
|
const updateSearch = (newSearch: Search) => {
|
||||||
setSearch(search);
|
setActiveCollection(ALL_SECTION);
|
||||||
|
setSearch(newSearch);
|
||||||
setSearchStats(null);
|
setSearchStats(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -563,11 +553,12 @@ export default function Gallery() {
|
||||||
attributes={dialogMessage}
|
attributes={dialogMessage}
|
||||||
/>
|
/>
|
||||||
<SearchBar
|
<SearchBar
|
||||||
isOpen={searchMode}
|
isOpen={isInSearchMode}
|
||||||
setOpen={setSearchMode}
|
setOpen={setIsInSearchMode}
|
||||||
loadingBar={loadingBar}
|
loadingBar={loadingBar}
|
||||||
isFirstFetch={isFirstFetch}
|
isFirstFetch={isFirstFetch}
|
||||||
collections={collections}
|
collections={collections}
|
||||||
|
files={files}
|
||||||
setActiveCollection={setActiveCollection}
|
setActiveCollection={setActiveCollection}
|
||||||
setSearch={updateSearch}
|
setSearch={updateSearch}
|
||||||
searchStats={searchStats}
|
searchStats={searchStats}
|
||||||
|
@ -575,7 +566,7 @@ export default function Gallery() {
|
||||||
<Collections
|
<Collections
|
||||||
collections={collections}
|
collections={collections}
|
||||||
collectionAndTheirLatestFile={collectionsAndTheirLatestFile}
|
collectionAndTheirLatestFile={collectionsAndTheirLatestFile}
|
||||||
searchMode={searchMode}
|
isInSearchMode={isInSearchMode}
|
||||||
activeCollection={activeCollection}
|
activeCollection={activeCollection}
|
||||||
setActiveCollection={setActiveCollection}
|
setActiveCollection={setActiveCollection}
|
||||||
syncWithRemote={syncWithRemote}
|
syncWithRemote={syncWithRemote}
|
||||||
|
@ -639,13 +630,16 @@ export default function Gallery() {
|
||||||
isFirstLoad={isFirstLoad}
|
isFirstLoad={isFirstLoad}
|
||||||
openFileUploader={openFileUploader}
|
openFileUploader={openFileUploader}
|
||||||
loadingBar={loadingBar}
|
loadingBar={loadingBar}
|
||||||
searchMode={searchMode}
|
isInSearchMode={isInSearchMode}
|
||||||
search={search}
|
search={search}
|
||||||
setSearchStats={setSearchStats}
|
setSearchStats={setSearchStats}
|
||||||
deleted={deleted}
|
deleted={deleted}
|
||||||
setDialogMessage={setDialogMessage}
|
setDialogMessage={setDialogMessage}
|
||||||
activeCollection={activeCollection}
|
activeCollection={activeCollection}
|
||||||
isSharedCollection={isSharedCollectionActive}
|
isSharedCollection={isSharedCollection(
|
||||||
|
activeCollection,
|
||||||
|
collections
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
{selected.count > 0 &&
|
{selected.count > 0 &&
|
||||||
selected.collectionID === activeCollection && (
|
selected.collectionID === activeCollection && (
|
||||||
|
@ -688,7 +682,10 @@ export default function Gallery() {
|
||||||
count={selected.count}
|
count={selected.count}
|
||||||
clearSelection={clearSelection}
|
clearSelection={clearSelection}
|
||||||
activeCollection={activeCollection}
|
activeCollection={activeCollection}
|
||||||
isFavoriteCollection={isFavCollectionActive}
|
isFavoriteCollection={isFavoriteCollection(
|
||||||
|
activeCollection,
|
||||||
|
collections
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{activeCollection === TRASH_SECTION && trash?.length > 0 && (
|
{activeCollection === TRASH_SECTION && trash?.length > 0 && (
|
||||||
|
|
|
@ -23,7 +23,6 @@ export enum CollectionType {
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLLECTION_UPDATION_TIME = 'collection-updation-time';
|
const COLLECTION_UPDATION_TIME = 'collection-updation-time';
|
||||||
const FAV_COLLECTION = 'fav-collection';
|
|
||||||
const COLLECTIONS = 'collections';
|
const COLLECTIONS = 'collections';
|
||||||
|
|
||||||
export interface Collection {
|
export interface Collection {
|
||||||
|
@ -365,7 +364,11 @@ export const addToFavorites = async (file: File) => {
|
||||||
'Favorites',
|
'Favorites',
|
||||||
CollectionType.favorites
|
CollectionType.favorites
|
||||||
);
|
);
|
||||||
await localForage.setItem(FAV_COLLECTION, favCollection);
|
const localCollections = await getLocalCollections();
|
||||||
|
await localForage.setItem(COLLECTIONS, [
|
||||||
|
...localCollections,
|
||||||
|
favCollection,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
await addToCollection(favCollection, [file]);
|
await addToCollection(favCollection, [file]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -24,7 +24,7 @@ class DownloadManager {
|
||||||
return URL.createObjectURL(await cacheResp.blob());
|
return URL.createObjectURL(await cacheResp.blob());
|
||||||
}
|
}
|
||||||
if (!this.thumbnailObjectUrlPromise.get(file.id)) {
|
if (!this.thumbnailObjectUrlPromise.get(file.id)) {
|
||||||
const downloadPromise = this._downloadThumb(
|
const downloadPromise = this.downloadThumb(
|
||||||
token,
|
token,
|
||||||
thumbnailCache,
|
thumbnailCache,
|
||||||
file
|
file
|
||||||
|
@ -35,14 +35,28 @@ class DownloadManager {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.thumbnailObjectUrlPromise.delete(file.id);
|
this.thumbnailObjectUrlPromise.delete(file.id);
|
||||||
logError(e, 'get preview Failed');
|
logError(e, 'get preview Failed');
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_downloadThumb = async (
|
private downloadThumb = async (
|
||||||
token: string,
|
token: string,
|
||||||
thumbnailCache: Cache,
|
thumbnailCache: Cache,
|
||||||
file: File
|
file: File
|
||||||
) => {
|
) => {
|
||||||
|
const thumb = await this.getThumbnail(token, file);
|
||||||
|
try {
|
||||||
|
await thumbnailCache.put(
|
||||||
|
file.id.toString(),
|
||||||
|
new Response(new Blob([thumb]))
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// TODO: handle storage full exception.
|
||||||
|
}
|
||||||
|
return URL.createObjectURL(new Blob([thumb]));
|
||||||
|
};
|
||||||
|
|
||||||
|
getThumbnail = async (token: string, file: File) => {
|
||||||
const resp = await HTTPService.get(
|
const resp = await HTTPService.get(
|
||||||
getThumbnailUrl(file.id),
|
getThumbnailUrl(file.id),
|
||||||
null,
|
null,
|
||||||
|
@ -50,43 +64,38 @@ class DownloadManager {
|
||||||
{ responseType: 'arraybuffer' }
|
{ responseType: 'arraybuffer' }
|
||||||
);
|
);
|
||||||
const worker = await new CryptoWorker();
|
const worker = await new CryptoWorker();
|
||||||
const decrypted: any = await worker.decryptThumbnail(
|
const decrypted: Uint8Array = await worker.decryptThumbnail(
|
||||||
new Uint8Array(resp.data),
|
new Uint8Array(resp.data),
|
||||||
await worker.fromB64(file.thumbnail.decryptionHeader),
|
await worker.fromB64(file.thumbnail.decryptionHeader),
|
||||||
file.key
|
file.key
|
||||||
);
|
);
|
||||||
try {
|
return decrypted;
|
||||||
await thumbnailCache.put(
|
|
||||||
file.id.toString(),
|
|
||||||
new Response(new Blob([decrypted]))
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
// TODO: handle storage full exception.
|
|
||||||
}
|
|
||||||
return URL.createObjectURL(new Blob([decrypted]));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
getFile = async (file: File, forPreview = false) => {
|
getFile = async (file: File, forPreview = false) => {
|
||||||
|
let fileUID: string;
|
||||||
|
if (file.metadata.fileType === FILE_TYPE.VIDEO) {
|
||||||
|
fileUID = file.id.toString();
|
||||||
|
} else {
|
||||||
|
fileUID = `${file.id}_forPreview=${forPreview}`;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const getFilePromise = (async () => {
|
const getFilePromise = async () => {
|
||||||
const fileStream = await this.downloadFile(file);
|
const fileStream = await this.downloadFile(file);
|
||||||
let fileBlob = await new Response(fileStream).blob();
|
let fileBlob = await new Response(fileStream).blob();
|
||||||
if (forPreview) {
|
if (forPreview) {
|
||||||
fileBlob = await convertForPreview(file, fileBlob);
|
fileBlob = await convertForPreview(file, fileBlob);
|
||||||
}
|
}
|
||||||
return URL.createObjectURL(fileBlob);
|
return URL.createObjectURL(fileBlob);
|
||||||
})();
|
};
|
||||||
if (!this.fileObjectUrlPromise.get(`${file.id}_${forPreview}`)) {
|
if (!this.fileObjectUrlPromise.get(fileUID)) {
|
||||||
this.fileObjectUrlPromise.set(
|
this.fileObjectUrlPromise.set(fileUID, getFilePromise());
|
||||||
`${file.id}_${forPreview}`,
|
|
||||||
getFilePromise
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return await this.fileObjectUrlPromise.get(
|
return await this.fileObjectUrlPromise.get(fileUID);
|
||||||
`${file.id}_${forPreview}`
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
this.fileObjectUrlPromise.delete(fileUID);
|
||||||
logError(e, 'Failed to get File');
|
logError(e, 'Failed to get File');
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -44,8 +44,8 @@ class FFmpegService {
|
||||||
|
|
||||||
async function generateThumbnailHelper(ffmpeg: FFmpeg, file: File) {
|
async function generateThumbnailHelper(ffmpeg: FFmpeg, file: File) {
|
||||||
try {
|
try {
|
||||||
const inputFileName = `${Date.now().toString}-${file.name}`;
|
const inputFileName = `${Date.now().toString()}-${file.name}`;
|
||||||
const thumbFileName = `${Date.now().toString}-thumb.png`;
|
const thumbFileName = `${Date.now().toString()}-thumb.jpeg`;
|
||||||
ffmpeg.FS(
|
ffmpeg.FS(
|
||||||
'writeFile',
|
'writeFile',
|
||||||
inputFileName,
|
inputFileName,
|
||||||
|
@ -62,6 +62,8 @@ async function generateThumbnailHelper(ffmpeg: FFmpeg, file: File) {
|
||||||
`00:00:0${seekTime.toFixed(3)}`,
|
`00:00:0${seekTime.toFixed(3)}`,
|
||||||
'-vframes',
|
'-vframes',
|
||||||
'1',
|
'1',
|
||||||
|
'-vf',
|
||||||
|
'scale=-1:720',
|
||||||
thumbFileName
|
thumbFileName
|
||||||
);
|
);
|
||||||
thumb = ffmpeg.FS('readFile', thumbFileName);
|
thumb = ffmpeg.FS('readFile', thumbFileName);
|
||||||
|
|
141
src/services/migrateThumbnailService.ts
Normal file
141
src/services/migrateThumbnailService.ts
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
import downloadManager from 'services/downloadManager';
|
||||||
|
import { fileAttribute, FILE_TYPE, getLocalFiles } from 'services/fileService';
|
||||||
|
import { generateThumbnail } from 'services/upload/thumbnailService';
|
||||||
|
import { getToken } from 'utils/common/key';
|
||||||
|
import { logError } from 'utils/sentry';
|
||||||
|
import { getEndpoint } from 'utils/common/apiUtil';
|
||||||
|
import HTTPService from 'services/HTTPService';
|
||||||
|
import CryptoWorker from 'utils/crypto';
|
||||||
|
import uploadHttpClient from 'services/upload/uploadHttpClient';
|
||||||
|
import { EncryptionResult, UploadURL } from 'services/upload/uploadService';
|
||||||
|
import { SetProgressTracker } from 'components/FixLargeThumbnail';
|
||||||
|
|
||||||
|
const ENDPOINT = getEndpoint();
|
||||||
|
const REPLACE_THUMBNAIL_THRESHOLD = 500 * 1024; // 500KB
|
||||||
|
export async function getLargeThumbnailFiles() {
|
||||||
|
try {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const resp = await HTTPService.get(
|
||||||
|
`${ENDPOINT}/files/large-thumbnails`,
|
||||||
|
{
|
||||||
|
threshold: REPLACE_THUMBNAIL_THRESHOLD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'X-Auth-Token': token,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return resp.data.largeThumbnailFiles as number[];
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'failed to get large thumbnail files');
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function replaceThumbnail(
|
||||||
|
setProgressTracker: SetProgressTracker,
|
||||||
|
largeThumbnailFileIDs: Set<number>
|
||||||
|
) {
|
||||||
|
let completedWithError = false;
|
||||||
|
try {
|
||||||
|
const token = getToken();
|
||||||
|
const worker = await new CryptoWorker();
|
||||||
|
const files = await getLocalFiles();
|
||||||
|
const largeThumbnailFiles = files.filter((file) =>
|
||||||
|
largeThumbnailFileIDs.has(file.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (largeThumbnailFiles.length === 0) {
|
||||||
|
return completedWithError;
|
||||||
|
}
|
||||||
|
setProgressTracker({ current: 0, total: largeThumbnailFiles.length });
|
||||||
|
const uploadURLs: UploadURL[] = [];
|
||||||
|
uploadHttpClient.fetchUploadURLs(
|
||||||
|
largeThumbnailFiles.length,
|
||||||
|
uploadURLs
|
||||||
|
);
|
||||||
|
for (const [idx, file] of largeThumbnailFiles.entries()) {
|
||||||
|
try {
|
||||||
|
setProgressTracker({
|
||||||
|
current: idx,
|
||||||
|
total: largeThumbnailFiles.length,
|
||||||
|
});
|
||||||
|
const originalThumbnail = await downloadManager.getThumbnail(
|
||||||
|
token,
|
||||||
|
file
|
||||||
|
);
|
||||||
|
const dummyImageFile = new globalThis.File(
|
||||||
|
[originalThumbnail],
|
||||||
|
file.metadata.title
|
||||||
|
);
|
||||||
|
const { thumbnail: newThumbnail } = await generateThumbnail(
|
||||||
|
worker,
|
||||||
|
dummyImageFile,
|
||||||
|
FILE_TYPE.IMAGE,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
const newUploadedThumbnail = await uploadThumbnail(
|
||||||
|
worker,
|
||||||
|
file.key,
|
||||||
|
newThumbnail,
|
||||||
|
uploadURLs.pop()
|
||||||
|
);
|
||||||
|
await updateThumbnail(file.id, newUploadedThumbnail);
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'failed to replace a thumbnail');
|
||||||
|
completedWithError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'replace Thumbnail function failed');
|
||||||
|
completedWithError = true;
|
||||||
|
}
|
||||||
|
return completedWithError;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadThumbnail(
|
||||||
|
worker,
|
||||||
|
fileKey: string,
|
||||||
|
updatedThumbnail: Uint8Array,
|
||||||
|
uploadURL: UploadURL
|
||||||
|
): Promise<fileAttribute> {
|
||||||
|
const { file: encryptedThumbnail }: EncryptionResult =
|
||||||
|
await worker.encryptThumbnail(updatedThumbnail, fileKey);
|
||||||
|
|
||||||
|
const thumbnailObjectKey = await uploadHttpClient.putFile(
|
||||||
|
uploadURL,
|
||||||
|
encryptedThumbnail.encryptedData as Uint8Array,
|
||||||
|
() => {}
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
objectKey: thumbnailObjectKey,
|
||||||
|
decryptionHeader: encryptedThumbnail.decryptionHeader,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateThumbnail(
|
||||||
|
fileID: number,
|
||||||
|
newThumbnail: fileAttribute
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await HTTPService.put(
|
||||||
|
`${ENDPOINT}/files/thumbnail`,
|
||||||
|
{
|
||||||
|
fileID: fileID,
|
||||||
|
thumbnail: newThumbnail,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
'X-Auth-Token': token,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'failed to update thumbnail');
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,9 @@ import { getToken } from 'utils/common/key';
|
||||||
import { DateValue, Suggestion, SuggestionType } from 'components/SearchBar';
|
import { DateValue, Suggestion, SuggestionType } from 'components/SearchBar';
|
||||||
import HTTPService from './HTTPService';
|
import HTTPService from './HTTPService';
|
||||||
import { Collection } from './collectionService';
|
import { Collection } from './collectionService';
|
||||||
|
import { File } from './fileService';
|
||||||
|
import { User } from './userService';
|
||||||
|
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||||
|
|
||||||
const ENDPOINT = getEndpoint();
|
const ENDPOINT = getEndpoint();
|
||||||
const DIGITS = new Set(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']);
|
const DIGITS = new Set(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']);
|
||||||
|
@ -110,3 +113,25 @@ export function searchCollection(
|
||||||
collection.name.toLowerCase().includes(searchPhrase)
|
collection.name.toLowerCase().includes(searchPhrase)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function searchFiles(searchPhrase: string, files: File[]) {
|
||||||
|
const user: User = getData(LS_KEYS.USER) ?? {};
|
||||||
|
const idSet = new Set();
|
||||||
|
return files
|
||||||
|
.map((file, idx) => ({
|
||||||
|
title: file.metadata.title,
|
||||||
|
index: idx,
|
||||||
|
type: file.metadata.fileType,
|
||||||
|
ownerID: file.ownerID,
|
||||||
|
id: file.id,
|
||||||
|
}))
|
||||||
|
.filter((file) => {
|
||||||
|
if (file.ownerID === user.id && !idSet.has(file.id)) {
|
||||||
|
idSet.add(file.id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.filter(({ title }) => title.toLowerCase().includes(searchPhrase))
|
||||||
|
.slice(0, 4);
|
||||||
|
}
|
||||||
|
|
|
@ -3,13 +3,21 @@ import { CustomError, errorWithContext } from 'utils/common/errorUtil';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import { BLACK_THUMBNAIL_BASE64 } from '../../../public/images/black-thumbnail-b64';
|
import { BLACK_THUMBNAIL_BASE64 } from '../../../public/images/black-thumbnail-b64';
|
||||||
import FFmpegService from 'services/ffmpegService';
|
import FFmpegService from 'services/ffmpegService';
|
||||||
|
import { convertToHumanReadable } from 'utils/billingUtil';
|
||||||
|
|
||||||
const THUMBNAIL_HEIGHT = 720;
|
const MAX_THUMBNAIL_DIMENSION = 720;
|
||||||
const MAX_ATTEMPTS = 3;
|
const MIN_COMPRESSION_PERCENTAGE_SIZE_DIFF = 10;
|
||||||
const MIN_THUMBNAIL_SIZE = 50000;
|
export 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 = 10 * 1000;
|
||||||
|
|
||||||
|
interface Dimension {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
export async function generateThumbnail(
|
export async function generateThumbnail(
|
||||||
worker,
|
worker,
|
||||||
file: globalThis.File,
|
file: globalThis.File,
|
||||||
|
@ -26,7 +34,12 @@ export async function generateThumbnail(
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const thumb = await FFmpegService.generateThumbnail(file);
|
const thumb = await FFmpegService.generateThumbnail(file);
|
||||||
return { thumbnail: thumb, hasStaticThumbnail: false };
|
const dummyImageFile = new File([thumb], file.name);
|
||||||
|
canvas = await generateImageThumbnail(
|
||||||
|
worker,
|
||||||
|
dummyImageFile,
|
||||||
|
isHEIC
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
canvas = await generateVideoThumbnail(file);
|
canvas = await generateVideoThumbnail(file);
|
||||||
}
|
}
|
||||||
|
@ -74,16 +87,22 @@ export async function generateImageThumbnail(
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
image.onload = () => {
|
image.onload = () => {
|
||||||
try {
|
try {
|
||||||
const thumbnailWidth =
|
const imageDimension = {
|
||||||
(image.width * THUMBNAIL_HEIGHT) / image.height;
|
width: image.width,
|
||||||
canvas.width = thumbnailWidth;
|
height: image.height,
|
||||||
canvas.height = THUMBNAIL_HEIGHT;
|
};
|
||||||
|
const thumbnailDimension = calculateThumbnailDimension(
|
||||||
|
imageDimension,
|
||||||
|
MAX_THUMBNAIL_DIMENSION
|
||||||
|
);
|
||||||
|
canvas.width = thumbnailDimension.width;
|
||||||
|
canvas.height = thumbnailDimension.height;
|
||||||
canvasCTX.drawImage(
|
canvasCTX.drawImage(
|
||||||
image,
|
image,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
thumbnailWidth,
|
thumbnailDimension.width,
|
||||||
THUMBNAIL_HEIGHT
|
thumbnailDimension.height
|
||||||
);
|
);
|
||||||
image = null;
|
image = null;
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
@ -126,16 +145,22 @@ export async function generateVideoThumbnail(file: globalThis.File) {
|
||||||
if (!video) {
|
if (!video) {
|
||||||
throw Error('video load failed');
|
throw Error('video load failed');
|
||||||
}
|
}
|
||||||
const thumbnailWidth =
|
const videoDimension = {
|
||||||
(video.videoWidth * THUMBNAIL_HEIGHT) / video.videoHeight;
|
width: video.videoWidth,
|
||||||
canvas.width = thumbnailWidth;
|
height: video.videoHeight,
|
||||||
canvas.height = THUMBNAIL_HEIGHT;
|
};
|
||||||
|
const thumbnailDimension = calculateThumbnailDimension(
|
||||||
|
videoDimension,
|
||||||
|
MAX_THUMBNAIL_DIMENSION
|
||||||
|
);
|
||||||
|
canvas.width = thumbnailDimension.width;
|
||||||
|
canvas.height = thumbnailDimension.height;
|
||||||
canvasCTX.drawImage(
|
canvasCTX.drawImage(
|
||||||
video,
|
video,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
thumbnailWidth,
|
thumbnailDimension.width,
|
||||||
THUMBNAIL_HEIGHT
|
thumbnailDimension.height
|
||||||
);
|
);
|
||||||
video = null;
|
video = null;
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
@ -166,11 +191,14 @@ export async function generateVideoThumbnail(file: globalThis.File) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function thumbnailCanvasToBlob(canvas: HTMLCanvasElement) {
|
async function thumbnailCanvasToBlob(canvas: HTMLCanvasElement) {
|
||||||
let thumbnailBlob = null;
|
let thumbnailBlob: Blob = null;
|
||||||
let attempts = 0;
|
let prevSize = Number.MAX_SAFE_INTEGER;
|
||||||
let quality = 1;
|
let quality = MAX_QUALITY;
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
if (thumbnailBlob) {
|
||||||
|
prevSize = thumbnailBlob.size;
|
||||||
|
}
|
||||||
thumbnailBlob = await new Promise((resolve) => {
|
thumbnailBlob = await new Promise((resolve) => {
|
||||||
canvas.toBlob(
|
canvas.toBlob(
|
||||||
function (blob) {
|
function (blob) {
|
||||||
|
@ -181,12 +209,49 @@ async function thumbnailCanvasToBlob(canvas: HTMLCanvasElement) {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
thumbnailBlob = thumbnailBlob ?? new Blob([]);
|
thumbnailBlob = thumbnailBlob ?? new Blob([]);
|
||||||
attempts++;
|
quality -= 0.1;
|
||||||
quality /= 2;
|
|
||||||
} while (
|
} while (
|
||||||
thumbnailBlob.size > MIN_THUMBNAIL_SIZE &&
|
quality >= MIN_QUALITY &&
|
||||||
attempts <= MAX_ATTEMPTS
|
thumbnailBlob.size > MAX_THUMBNAIL_SIZE &&
|
||||||
|
percentageSizeDiff(thumbnailBlob.size, prevSize) >=
|
||||||
|
MIN_COMPRESSION_PERCENTAGE_SIZE_DIFF
|
||||||
);
|
);
|
||||||
|
if (thumbnailBlob.size > MAX_THUMBNAIL_SIZE) {
|
||||||
|
logError(
|
||||||
|
Error('thumbnail_too_large'),
|
||||||
|
'thumbnail greater than max limit',
|
||||||
|
{ thumbnailSize: convertToHumanReadable(thumbnailBlob.size) }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return thumbnailBlob;
|
return thumbnailBlob;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function percentageSizeDiff(
|
||||||
|
newThumbnailSize: number,
|
||||||
|
oldThumbnailSize: number
|
||||||
|
) {
|
||||||
|
return ((oldThumbnailSize - newThumbnailSize) * 100) / oldThumbnailSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// method to calculate new size of image for limiting it to maximum width and height, maintaining aspect ratio
|
||||||
|
// returns {0,0} for invalid inputs
|
||||||
|
function calculateThumbnailDimension(
|
||||||
|
originalDimension: Dimension,
|
||||||
|
maxDimension: number
|
||||||
|
): Dimension {
|
||||||
|
if (originalDimension.height === 0 || originalDimension.width === 0) {
|
||||||
|
return { width: 0, height: 0 };
|
||||||
|
}
|
||||||
|
const widthScaleFactor = maxDimension / originalDimension.width;
|
||||||
|
const heightScaleFactor = maxDimension / originalDimension.height;
|
||||||
|
const scaleFactor = Math.min(widthScaleFactor, heightScaleFactor);
|
||||||
|
const thumbnailDimension = {
|
||||||
|
width: Math.round(originalDimension.width * scaleFactor),
|
||||||
|
height: Math.round(originalDimension.height * scaleFactor),
|
||||||
|
};
|
||||||
|
if (thumbnailDimension.width === 0 || thumbnailDimension.height === 0) {
|
||||||
|
return { width: 0, height: 0 };
|
||||||
|
}
|
||||||
|
return thumbnailDimension;
|
||||||
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
import { decodeMotionPhoto } from 'services/motionPhotoService';
|
import { decodeMotionPhoto } from 'services/motionPhotoService';
|
||||||
import { getMimeTypeFromBlob } from 'services/upload/readFileService';
|
import { getMimeTypeFromBlob } from 'services/upload/readFileService';
|
||||||
import { EncryptionResult } from 'services/upload/uploadService';
|
import { EncryptionResult } from 'services/upload/uploadService';
|
||||||
|
import DownloadManger from 'services/downloadManager';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import { User } from 'services/userService';
|
import { User } from 'services/userService';
|
||||||
import CryptoWorker from 'utils/crypto';
|
import CryptoWorker from 'utils/crypto';
|
||||||
|
@ -36,6 +37,20 @@ export function downloadAsFile(filename: string, content: string) {
|
||||||
a.remove();
|
a.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function downloadFile(file) {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.style.display = 'none';
|
||||||
|
a.href = await DownloadManger.getFile(file);
|
||||||
|
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||||
|
a.download = fileNameWithoutExtension(file.metadata.title) + '.zip';
|
||||||
|
} else {
|
||||||
|
a.download = file.metadata.title;
|
||||||
|
}
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
}
|
||||||
|
|
||||||
export function fileIsHEIC(mimeType: string) {
|
export function fileIsHEIC(mimeType: string) {
|
||||||
return (
|
return (
|
||||||
mimeType.toLowerCase().endsWith(TYPE_HEIC) ||
|
mimeType.toLowerCase().endsWith(TYPE_HEIC) ||
|
||||||
|
|
15
src/utils/photoFrame/index.ts
Normal file
15
src/utils/photoFrame/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
const WAIT_FOR_VIDEO_PLAYBACK = 1 * 1000;
|
||||||
|
|
||||||
|
export async function isPlaybackPossible(url: string): Promise<boolean> {
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
const t = setTimeout(() => {
|
||||||
|
resolve(false);
|
||||||
|
}, WAIT_FOR_VIDEO_PLAYBACK);
|
||||||
|
const video = document.createElement('video');
|
||||||
|
video.addEventListener('canplay', function () {
|
||||||
|
clearTimeout(t);
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
video.src = url;
|
||||||
|
});
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ export const logError = (
|
||||||
) => {
|
) => {
|
||||||
const err = errorWithContext(e, msg);
|
const err = errorWithContext(e, msg);
|
||||||
if (!process.env.NEXT_PUBLIC_SENTRY_ENV) {
|
if (!process.env.NEXT_PUBLIC_SENTRY_ENV) {
|
||||||
console.log(err);
|
console.log(e);
|
||||||
}
|
}
|
||||||
Sentry.captureException(err, {
|
Sentry.captureException(err, {
|
||||||
level: Sentry.Severity.Info,
|
level: Sentry.Severity.Info,
|
||||||
|
@ -18,6 +18,7 @@ export const logError = (
|
||||||
...(info && {
|
...(info && {
|
||||||
info: info,
|
info: info,
|
||||||
}),
|
}),
|
||||||
|
rootCause: { message: e?.message },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,6 +12,7 @@ export enum LS_KEYS {
|
||||||
SHOW_BACK_BUTTON = 'showBackButton',
|
SHOW_BACK_BUTTON = 'showBackButton',
|
||||||
EXPORT = 'export',
|
EXPORT = 'export',
|
||||||
AnonymizeUserID = 'anonymizedUserID',
|
AnonymizeUserID = 'anonymizedUserID',
|
||||||
|
THUMBNAIL_FIX_STATE = 'thumbnailFixState',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setData = (key: LS_KEYS, value: object) => {
|
export const setData = (key: LS_KEYS, value: object) => {
|
||||||
|
|
|
@ -548,9 +548,6 @@ const englishConstants = {
|
||||||
MOVE: 'move',
|
MOVE: 'move',
|
||||||
ADD: 'add',
|
ADD: 'add',
|
||||||
SORT: 'sort',
|
SORT: 'sort',
|
||||||
SORT_BY_LATEST_PHOTO: 'most recent photo',
|
|
||||||
SORT_BY_MODIFICATION_TIME: 'last modified',
|
|
||||||
SORT_BY_COLLECTION_NAME: 'album title',
|
|
||||||
REMOVE: 'remove',
|
REMOVE: 'remove',
|
||||||
CONFIRM_REMOVE: 'confirm removal',
|
CONFIRM_REMOVE: 'confirm removal',
|
||||||
TRASH: 'trash',
|
TRASH: 'trash',
|
||||||
|
@ -571,14 +568,31 @@ const englishConstants = {
|
||||||
|
|
||||||
CONFIRM_REMOVE_MESSAGE: () => (
|
CONFIRM_REMOVE_MESSAGE: () => (
|
||||||
<>
|
<>
|
||||||
<p>
|
<p>are you sure you want to remove these files from the album?</p>
|
||||||
are you sure you want to remove these files from the collection?
|
|
||||||
</p>
|
|
||||||
<p>
|
<p>
|
||||||
all files that are unique to this album will be moved to trash
|
all files that are unique to this album will be moved to trash
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
SORT_BY_LATEST_PHOTO: 'recent photo',
|
||||||
|
SORT_BY_MODIFICATION_TIME: 'last updated',
|
||||||
|
SORT_BY_COLLECTION_NAME: 'album name',
|
||||||
|
FIX_LARGE_THUMBNAILS: 'compress thumbnails',
|
||||||
|
THUMBNAIL_REPLACED: 'thumbnails compressed',
|
||||||
|
FIX: 'compress',
|
||||||
|
FIX_LATER: 'compress later',
|
||||||
|
REPLACE_THUMBNAIL_NOT_STARTED: () => (
|
||||||
|
<>
|
||||||
|
some of your videos thumbnails can be compressed to save space.
|
||||||
|
would you like ente to compress them?
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
REPLACE_THUMBNAIL_COMPLETED: () => (
|
||||||
|
<>successfully compressed all thumbnails</>
|
||||||
|
),
|
||||||
|
REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR: () => (
|
||||||
|
<>could not compress some of your thumbnails, please retry</>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default englishConstants;
|
export default englishConstants;
|
||||||
|
|
Loading…
Reference in a new issue