Merge branch 'master' into release
This commit is contained in:
commit
b497d242df
|
@ -18,7 +18,6 @@ import { VariableSizeList as List } from 'react-window';
|
|||
import PhotoSwipe from 'components/PhotoSwipe/PhotoSwipe';
|
||||
import { isInsideBox, isSameDay as isSameDayAnyYear } from 'utils/search';
|
||||
import { SetDialogMessage } from './MessageDialog';
|
||||
import { CustomError } from 'utils/common/errorUtil';
|
||||
import {
|
||||
GAP_BTW_TILES,
|
||||
DATE_CONTAINER_HEIGHT,
|
||||
|
@ -30,10 +29,10 @@ import {
|
|||
import { fileIsArchived } from 'utils/file';
|
||||
import { ALL_SECTION, ARCHIVE_SECTION } from './pages/gallery/Collections';
|
||||
import { isSharedFile } from 'utils/file';
|
||||
import { isPlaybackPossible } from 'utils/photoFrame';
|
||||
|
||||
const NO_OF_PAGES = 2;
|
||||
const A_DAY = 24 * 60 * 60 * 1000;
|
||||
const WAIT_FOR_VIDEO_PLAYBACK = 1 * 1000;
|
||||
|
||||
interface TimeStampListItem {
|
||||
itemType: ITEM_TYPE;
|
||||
|
@ -146,7 +145,7 @@ interface Props {
|
|||
isFirstLoad;
|
||||
openFileUploader;
|
||||
loadingBar;
|
||||
searchMode: boolean;
|
||||
isInSearchMode: boolean;
|
||||
search: Search;
|
||||
setSearchStats: setSearchStats;
|
||||
deleted?: number[];
|
||||
|
@ -165,11 +164,10 @@ const PhotoFrame = ({
|
|||
isFirstLoad,
|
||||
openFileUploader,
|
||||
loadingBar,
|
||||
searchMode,
|
||||
isInSearchMode,
|
||||
search,
|
||||
setSearchStats,
|
||||
deleted,
|
||||
setDialogMessage,
|
||||
activeCollection,
|
||||
isSharedCollection,
|
||||
}: Props) => {
|
||||
|
@ -181,7 +179,7 @@ const PhotoFrame = ({
|
|||
const listRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchMode) {
|
||||
if (isInSearchMode) {
|
||||
setSearchStats({
|
||||
resultCount: filteredData.length,
|
||||
timeTaken: (Date.now() - startTime) / 1000,
|
||||
|
@ -225,21 +223,33 @@ const PhotoFrame = ({
|
|||
setFiles(files);
|
||||
};
|
||||
|
||||
const updateSrcUrl = (index: number, url: string) => {
|
||||
const updateSrcUrl = async (index: number, url: string) => {
|
||||
files[index] = {
|
||||
...files[index],
|
||||
src: url,
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
};
|
||||
if (files[index].metadata.fileType === FILE_TYPE.VIDEO) {
|
||||
files[index].html = `
|
||||
if (await isPlaybackPossible(url)) {
|
||||
files[index].html = `
|
||||
<video controls>
|
||||
<source src="${url}" />
|
||||
Your browser does not support the video tag.
|
||||
</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);
|
||||
};
|
||||
|
@ -313,66 +323,11 @@ const PhotoFrame = ({
|
|||
url = await DownloadManager.getFile(item, true);
|
||||
galleryContext.files.set(item.id, url);
|
||||
}
|
||||
updateSrcUrl(item.dataIndex, url);
|
||||
if (item.metadata.fileType === FILE_TYPE.VIDEO) {
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const video = document.createElement('video');
|
||||
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;
|
||||
await updateSrcUrl(item.dataIndex, url);
|
||||
item.html = files[item.dataIndex].html;
|
||||
item.src = files[item.dataIndex].src;
|
||||
item.w = files[item.dataIndex].w;
|
||||
item.h = files[item.dataIndex].h;
|
||||
try {
|
||||
instance.invalidateCurrItems();
|
||||
instance.updateSize(true);
|
||||
|
@ -515,7 +470,7 @@ const PhotoFrame = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
{!isFirstLoad && files.length === 0 && !searchMode ? (
|
||||
{!isFirstLoad && files.length === 0 && !isInSearchMode ? (
|
||||
<EmptyScreen>
|
||||
<img height={150} src="/images/gallery.png" />
|
||||
<div style={{ color: '#a6a6a6', marginTop: '16px' }}>
|
||||
|
@ -635,7 +590,7 @@ const PhotoFrame = ({
|
|||
return sum;
|
||||
})();
|
||||
files.length < 30 &&
|
||||
!searchMode &&
|
||||
!isInSearchMode &&
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.BANNER,
|
||||
banner: (
|
||||
|
|
|
@ -7,16 +7,15 @@ import {
|
|||
addToFavorites,
|
||||
removeFromFavorites,
|
||||
} from 'services/collectionService';
|
||||
import { File, FILE_TYPE } from 'services/fileService';
|
||||
import { File } from 'services/fileService';
|
||||
import constants from 'utils/strings/constants';
|
||||
import DownloadManger from 'services/downloadManager';
|
||||
import exifr from 'exifr';
|
||||
import Modal from 'react-bootstrap/Modal';
|
||||
import Button from 'react-bootstrap/Button';
|
||||
import Form from 'react-bootstrap/Form';
|
||||
import styled from 'styled-components';
|
||||
import events from './events';
|
||||
import { fileNameWithoutExtension, formatDateTime } from 'utils/file';
|
||||
import { downloadFile, formatDateTime } from 'utils/file';
|
||||
import { FormCheck } from 'react-bootstrap';
|
||||
import { prettyPrintExif } from 'utils/exif';
|
||||
|
||||
|
@ -296,21 +295,11 @@ function PhotoSwipe(props: Iprops) {
|
|||
setShowInfo(true);
|
||||
};
|
||||
|
||||
const downloadFile = async (file) => {
|
||||
const downloadFileHelper = async (file) => {
|
||||
const { loadingBar } = props;
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
loadingBar.current.continuousStart();
|
||||
a.href = await DownloadManger.getFile(file);
|
||||
await downloadFile(file);
|
||||
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;
|
||||
let { className } = props;
|
||||
|
@ -344,7 +333,7 @@ function PhotoSwipe(props: Iprops) {
|
|||
className="pswp-custom download-btn"
|
||||
title={constants.DOWNLOAD}
|
||||
onClick={() =>
|
||||
downloadFile(photoSwipe.currItem)
|
||||
downloadFileHelper(photoSwipe.currItem)
|
||||
}
|
||||
/>
|
||||
|
||||
|
|
|
@ -177,12 +177,12 @@ export default function SearchBar(props: Props) {
|
|||
break;
|
||||
case SuggestionType.COLLECTION:
|
||||
props.setActiveCollection(selectedOption.value as number);
|
||||
resetSearch(true);
|
||||
setValue(null);
|
||||
break;
|
||||
}
|
||||
};
|
||||
const resetSearch = async (force?: boolean) => {
|
||||
if (props.isOpen || force) {
|
||||
const resetSearch = () => {
|
||||
if (props.isOpen) {
|
||||
props.loadingBar.current?.continuousStart();
|
||||
props.setSearch({});
|
||||
setTimeout(() => {
|
||||
|
|
|
@ -35,7 +35,7 @@ interface CollectionProps {
|
|||
syncWithRemote: () => Promise<void>;
|
||||
setCollectionNamerAttributes: SetCollectionNamerAttributes;
|
||||
startLoadingBar: () => void;
|
||||
searchMode: boolean;
|
||||
isInSearchMode: boolean;
|
||||
collectionFilesCount: Map<number, number>;
|
||||
}
|
||||
|
||||
|
@ -87,6 +87,11 @@ const Chip = styled.button<{ active: boolean }>`
|
|||
}
|
||||
`;
|
||||
|
||||
const Hider = styled.div<{ hide: boolean }>`
|
||||
opacity: ${(props) => (props.hide ? '0' : '100')};
|
||||
height: ${(props) => (props.hide ? '0' : 'auto')};
|
||||
`;
|
||||
|
||||
export default function Collections(props: CollectionProps) {
|
||||
const { activeCollection, collections, setActiveCollection } = props;
|
||||
const [selectedCollectionID, setSelectedCollectionID] =
|
||||
|
@ -119,7 +124,7 @@ export default function Collections(props: CollectionProps) {
|
|||
|
||||
useEffect(() => {
|
||||
updateScrollObj();
|
||||
}, [collectionWrapperRef.current]);
|
||||
}, [collectionWrapperRef.current, props.isInSearchMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!collectionWrapperRef?.current) {
|
||||
|
@ -184,111 +189,104 @@ export default function Collections(props: CollectionProps) {
|
|||
};
|
||||
|
||||
return (
|
||||
!props.searchMode && (
|
||||
<>
|
||||
<CollectionShare
|
||||
show={collectionShareModalView}
|
||||
onHide={() => setCollectionShareModalView(false)}
|
||||
collection={getSelectedCollection(
|
||||
selectedCollectionID,
|
||||
props.collections
|
||||
<Hider hide={props.isInSearchMode}>
|
||||
<CollectionShare
|
||||
show={collectionShareModalView}
|
||||
onHide={() => setCollectionShareModalView(false)}
|
||||
collection={getSelectedCollection(
|
||||
selectedCollectionID,
|
||||
props.collections
|
||||
)}
|
||||
syncWithRemote={props.syncWithRemote}
|
||||
/>
|
||||
<CollectionBar>
|
||||
<CollectionContainer>
|
||||
{scrollObj.scrollLeft > 0 && (
|
||||
<NavigationButton
|
||||
scrollDirection={SCROLL_DIRECTION.LEFT}
|
||||
onClick={scrollCollection(SCROLL_DIRECTION.LEFT)}
|
||||
/>
|
||||
)}
|
||||
syncWithRemote={props.syncWithRemote}
|
||||
/>
|
||||
<CollectionBar>
|
||||
<CollectionContainer>
|
||||
{scrollObj.scrollLeft > 0 && (
|
||||
<NavigationButton
|
||||
scrollDirection={SCROLL_DIRECTION.LEFT}
|
||||
onClick={scrollCollection(
|
||||
SCROLL_DIRECTION.LEFT
|
||||
)}
|
||||
<Wrapper
|
||||
ref={collectionWrapperRef}
|
||||
onScroll={updateScrollObj}>
|
||||
<Chip
|
||||
active={activeCollection === ALL_SECTION}
|
||||
onClick={clickHandler(ALL_SECTION)}>
|
||||
{constants.ALL}
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '24px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Wrapper
|
||||
ref={collectionWrapperRef}
|
||||
onScroll={updateScrollObj}>
|
||||
<Chip
|
||||
active={activeCollection === ALL_SECTION}
|
||||
onClick={clickHandler(ALL_SECTION)}>
|
||||
{constants.ALL}
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '24px',
|
||||
}}
|
||||
/>
|
||||
</Chip>
|
||||
{sortCollections(
|
||||
collections,
|
||||
props.collectionAndTheirLatestFile,
|
||||
collectionSortBy
|
||||
).map((item) => (
|
||||
<OverlayTrigger
|
||||
key={item.id}
|
||||
placement="top"
|
||||
delay={{ show: 250, hide: 400 }}
|
||||
overlay={renderTooltip(item.id)}>
|
||||
<Chip
|
||||
ref={collectionChipsRef[item.id]}
|
||||
active={activeCollection === item.id}
|
||||
onClick={clickHandler(item.id)}>
|
||||
{item.name}
|
||||
{item.type !==
|
||||
CollectionType.favorites &&
|
||||
item.owner.id === user?.id ? (
|
||||
<OverlayTrigger
|
||||
rootClose
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
overlay={collectionOptions}>
|
||||
<OptionIcon
|
||||
onClick={() =>
|
||||
setSelectedCollectionID(
|
||||
item.id
|
||||
)
|
||||
}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '24px',
|
||||
}}
|
||||
</Chip>
|
||||
{sortCollections(
|
||||
collections,
|
||||
props.collectionAndTheirLatestFile,
|
||||
collectionSortBy
|
||||
).map((item) => (
|
||||
<OverlayTrigger
|
||||
key={item.id}
|
||||
placement="top"
|
||||
delay={{ show: 250, hide: 400 }}
|
||||
overlay={renderTooltip(item.id)}>
|
||||
<Chip
|
||||
ref={collectionChipsRef[item.id]}
|
||||
active={activeCollection === item.id}
|
||||
onClick={clickHandler(item.id)}>
|
||||
{item.name}
|
||||
{item.type !== CollectionType.favorites &&
|
||||
item.owner.id === user?.id ? (
|
||||
<OverlayTrigger
|
||||
rootClose
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
overlay={collectionOptions}>
|
||||
<OptionIcon
|
||||
onClick={() =>
|
||||
setSelectedCollectionID(
|
||||
item.id
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Chip>
|
||||
</OverlayTrigger>
|
||||
))}
|
||||
<Chip
|
||||
active={activeCollection === ARCHIVE_SECTION}
|
||||
onClick={clickHandler(ARCHIVE_SECTION)}>
|
||||
{constants.ARCHIVE}
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '24px',
|
||||
}}
|
||||
/>
|
||||
</Chip>
|
||||
</Wrapper>
|
||||
{scrollObj.scrollLeft <
|
||||
scrollObj.scrollWidth - scrollObj.clientWidth && (
|
||||
<NavigationButton
|
||||
scrollDirection={SCROLL_DIRECTION.RIGHT}
|
||||
onClick={scrollCollection(
|
||||
SCROLL_DIRECTION.RIGHT
|
||||
)}
|
||||
</OverlayTrigger>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '24px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Chip>
|
||||
</OverlayTrigger>
|
||||
))}
|
||||
<Chip
|
||||
active={activeCollection === ARCHIVE_SECTION}
|
||||
onClick={clickHandler(ARCHIVE_SECTION)}>
|
||||
{constants.ARCHIVE}
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '24px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</CollectionContainer>
|
||||
<CollectionSort
|
||||
setCollectionSortBy={setCollectionSortBy}
|
||||
activeSortBy={collectionSortBy}
|
||||
/>
|
||||
</CollectionBar>
|
||||
</>
|
||||
)
|
||||
</Chip>
|
||||
</Wrapper>
|
||||
{scrollObj.scrollLeft <
|
||||
scrollObj.scrollWidth - scrollObj.clientWidth && (
|
||||
<NavigationButton
|
||||
scrollDirection={SCROLL_DIRECTION.RIGHT}
|
||||
onClick={scrollCollection(SCROLL_DIRECTION.RIGHT)}
|
||||
/>
|
||||
)}
|
||||
</CollectionContainer>
|
||||
<CollectionSort
|
||||
setCollectionSortBy={setCollectionSortBy}
|
||||
activeSortBy={collectionSortBy}
|
||||
/>
|
||||
</CollectionBar>
|
||||
</Hider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -79,12 +79,33 @@ const GlobalStyles = createGlobalStyle`
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
.video-loading > div {
|
||||
.video-loading > div.spinner-border {
|
||||
position: relative;
|
||||
top: -50vh;
|
||||
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 {
|
||||
--primary: #e26f99,
|
||||
};
|
||||
|
|
|
@ -174,7 +174,7 @@ export default function Gallery() {
|
|||
});
|
||||
|
||||
const loadingBar = useRef(null);
|
||||
const [searchMode, setSearchMode] = useState(false);
|
||||
const [isInSearchMode, setIsInSearchMode] = useState(false);
|
||||
const [searchStats, setSearchStats] = useState(null);
|
||||
const syncInProgress = useRef(true);
|
||||
const resync = useRef(false);
|
||||
|
@ -491,8 +491,9 @@ export default function Gallery() {
|
|||
}
|
||||
};
|
||||
|
||||
const updateSearch = (search: Search) => {
|
||||
setSearch(search);
|
||||
const updateSearch = (newSearch: Search) => {
|
||||
setActiveCollection(ALL_SECTION);
|
||||
setSearch(newSearch);
|
||||
setSearchStats(null);
|
||||
};
|
||||
|
||||
|
@ -538,8 +539,8 @@ export default function Gallery() {
|
|||
attributes={dialogMessage}
|
||||
/>
|
||||
<SearchBar
|
||||
isOpen={searchMode}
|
||||
setOpen={setSearchMode}
|
||||
isOpen={isInSearchMode}
|
||||
setOpen={setIsInSearchMode}
|
||||
loadingBar={loadingBar}
|
||||
isFirstFetch={isFirstFetch}
|
||||
collections={collections}
|
||||
|
@ -550,7 +551,7 @@ export default function Gallery() {
|
|||
<Collections
|
||||
collections={collections}
|
||||
collectionAndTheirLatestFile={collectionsAndTheirLatestFile}
|
||||
searchMode={searchMode}
|
||||
isInSearchMode={isInSearchMode}
|
||||
activeCollection={activeCollection}
|
||||
setActiveCollection={setActiveCollection}
|
||||
syncWithRemote={syncWithRemote}
|
||||
|
@ -617,7 +618,7 @@ export default function Gallery() {
|
|||
isFirstLoad={isFirstLoad}
|
||||
openFileUploader={openFileUploader}
|
||||
loadingBar={loadingBar}
|
||||
searchMode={searchMode}
|
||||
isInSearchMode={isInSearchMode}
|
||||
search={search}
|
||||
setSearchStats={setSearchStats}
|
||||
deleted={deleted}
|
||||
|
|
|
@ -45,7 +45,7 @@ class FFmpegService {
|
|||
async function generateThumbnailHelper(ffmpeg: FFmpeg, file: File) {
|
||||
try {
|
||||
const inputFileName = `${Date.now().toString}-${file.name}`;
|
||||
const thumbFileName = `${Date.now().toString}-thumb.png`;
|
||||
const thumbFileName = `${Date.now().toString}-thumb.jpeg`;
|
||||
ffmpeg.FS(
|
||||
'writeFile',
|
||||
inputFileName,
|
||||
|
@ -62,6 +62,7 @@ async function generateThumbnailHelper(ffmpeg: FFmpeg, file: File) {
|
|||
`00:00:0${seekTime.toFixed(3)}`,
|
||||
'-vframes',
|
||||
'1',
|
||||
'-vf scale=512:512',
|
||||
thumbFileName
|
||||
);
|
||||
thumb = ffmpeg.FS('readFile', thumbFileName);
|
||||
|
|
|
@ -26,7 +26,12 @@ export async function generateThumbnail(
|
|||
} else {
|
||||
try {
|
||||
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) {
|
||||
canvas = await generateVideoThumbnail(file);
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
import { decodeMotionPhoto } from 'services/motionPhotoService';
|
||||
import { getMimeTypeFromBlob } from 'services/upload/readFileService';
|
||||
import { EncryptionResult } from 'services/upload/uploadService';
|
||||
import DownloadManger from 'services/downloadManager';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { User } from 'services/userService';
|
||||
import CryptoWorker from 'utils/crypto';
|
||||
|
@ -36,6 +37,20 @@ export function downloadAsFile(filename: string, content: string) {
|
|||
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) {
|
||||
return (
|
||||
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;
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue