Merge pull request #6 from ente-io/collection-ops
Collection Create and Add to favorite
This commit is contained in:
commit
5ea0b6caae
25 changed files with 1576 additions and 861 deletions
BIN
public/plus-sign.png
Normal file
BIN
public/plus-sign.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3 KiB |
22
src/components/FavButton.tsx
Normal file
22
src/components/FavButton.tsx
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const DropDiv = styled.div`
|
||||
|
@ -7,21 +7,35 @@ const DropDiv = styled.div`
|
|||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const FullScreenDropZone = ({
|
||||
children,
|
||||
closeModal,
|
||||
showModal,
|
||||
}) =>
|
||||
(
|
||||
<DropDiv onDragOver={(ev) => {
|
||||
ev.preventDefault();
|
||||
showModal();
|
||||
}} onDragLeave={(ev) => {
|
||||
ev.preventDefault();
|
||||
closeModal();
|
||||
}}>
|
||||
{children}
|
||||
</DropDiv>
|
||||
);
|
||||
type Props = React.PropsWithChildren<{
|
||||
showModal: () => void;
|
||||
closeModal: () => void;
|
||||
}>;
|
||||
|
||||
export default FullScreenDropZone;
|
||||
export default function FullScreenDropZone({ children, showModal, closeModal }: Props) {
|
||||
const closeTimer = useRef<number>();
|
||||
|
||||
const clearTimer = () => {
|
||||
if (closeTimer.current) {
|
||||
clearTimeout(closeTimer.current);
|
||||
}
|
||||
}
|
||||
|
||||
const onDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
clearTimer();
|
||||
showModal();
|
||||
}
|
||||
|
||||
const onDragLeave = (e) => {
|
||||
e.preventDefault();
|
||||
clearTimer();
|
||||
closeTimer.current = setTimeout(closeModal, 1000);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropDiv onDragOver={onDragOver} onDragLeave={onDragLeave}>
|
||||
{children}
|
||||
</DropDiv>
|
||||
);
|
||||
};
|
||||
|
|
196
src/components/PhotoSwipe/PhotoSwipe.tsx
Normal file
196
src/components/PhotoSwipe/PhotoSwipe.tsx
Normal file
|
@ -0,0 +1,196 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import Photoswipe from 'photoswipe';
|
||||
import PhotoswipeUIDefault from 'photoswipe/dist/photoswipe-ui-default';
|
||||
import classnames from 'classnames';
|
||||
import events from './events';
|
||||
import FavButton from 'components/FavButton';
|
||||
import { addToFavorites, removeFromFavorites } from 'services/collectionService';
|
||||
import { file } from 'services/fileService';
|
||||
|
||||
interface Iprops {
|
||||
isOpen: boolean
|
||||
items: any[];
|
||||
options?: Object;
|
||||
onClose?: () => void;
|
||||
gettingData?: (instance: any, index: number, item: file) => void;
|
||||
id?: string;
|
||||
className?: string;
|
||||
favItemIds: Set<number>;
|
||||
setFavItemIds: (favItemIds: Set<number>) => void;
|
||||
};
|
||||
|
||||
function PhotoSwipe(props: Iprops) {
|
||||
|
||||
let pswpElement;
|
||||
const [photoSwipe, setPhotoSwipe] = useState<Photoswipe<any>>();
|
||||
|
||||
const { isOpen } = props;
|
||||
const [isFav, setIsFav] = useState(false)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!pswpElement)
|
||||
return;
|
||||
if (isOpen)
|
||||
openPhotoSwipe();
|
||||
|
||||
}, [pswpElement]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pswpElement)
|
||||
return;
|
||||
if (isOpen) {
|
||||
openPhotoSwipe();
|
||||
}
|
||||
if (!isOpen) {
|
||||
closePhotoSwipe();
|
||||
}
|
||||
return () => {
|
||||
closePhotoSwipe();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
function updateFavButton() {
|
||||
console.log(this.currItem.id, props.favItemIds)
|
||||
setIsFav(isInFav(this?.currItem));
|
||||
}
|
||||
|
||||
|
||||
const openPhotoSwipe = () => {
|
||||
const { items, options } = props;
|
||||
let photoSwipe = new Photoswipe(pswpElement, PhotoswipeUIDefault, items, options);
|
||||
events.forEach((event) => {
|
||||
const callback = props[event];
|
||||
if (callback || event === 'destroy') {
|
||||
photoSwipe.listen(event, function (...args) {
|
||||
if (callback) {
|
||||
args.unshift(this);
|
||||
callback(...args);
|
||||
}
|
||||
if (event === 'destroy') {
|
||||
handleClose();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
photoSwipe.listen('beforeChange', updateFavButton);
|
||||
photoSwipe.init();
|
||||
setPhotoSwipe(photoSwipe);
|
||||
|
||||
};
|
||||
|
||||
const updateItems = (items = []) => {
|
||||
photoSwipe.items = [];
|
||||
items.forEach((item) => {
|
||||
photoSwipe.items.push(item);
|
||||
});
|
||||
photoSwipe.invalidateCurrItems();
|
||||
photoSwipe.updateSize(true);
|
||||
};
|
||||
|
||||
const closePhotoSwipe = () => {
|
||||
if (photoSwipe)
|
||||
photoSwipe.close();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
const { onClose } = props;
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
const isInFav = (file) => {
|
||||
const { favItemIds } = props;
|
||||
if (favItemIds && file) {
|
||||
return favItemIds.has(file.id);
|
||||
}
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
const onFavClick = async (file) => {
|
||||
const { favItemIds, setFavItemIds } = props;
|
||||
if (!isInFav(file)) {
|
||||
favItemIds.add(file.id);
|
||||
await addToFavorites(file);
|
||||
console.log("added to Favorites");
|
||||
setIsFav(true);
|
||||
setFavItemIds(favItemIds);
|
||||
}
|
||||
else {
|
||||
favItemIds.delete(file.id);
|
||||
await removeFromFavorites(file)
|
||||
console.log("removed from Favorites");
|
||||
setIsFav(false);
|
||||
setFavItemIds(favItemIds);
|
||||
|
||||
}
|
||||
}
|
||||
const { id } = props;
|
||||
let { className } = props;
|
||||
className = classnames(['pswp', className]).trim();
|
||||
return (
|
||||
<div
|
||||
id={id}
|
||||
className={className}
|
||||
tabIndex={Number("-1")}
|
||||
role="dialog"
|
||||
aria-hidden="true"
|
||||
ref={(node) => {
|
||||
pswpElement = node;
|
||||
}}
|
||||
>
|
||||
<div className="pswp__bg" />
|
||||
<div className="pswp__scroll-wrap">
|
||||
<div className="pswp__container">
|
||||
<div className="pswp__item" />
|
||||
<div className="pswp__item" />
|
||||
<div className="pswp__item" />
|
||||
</div>
|
||||
<div className="pswp__ui pswp__ui--hidden">
|
||||
<div className="pswp__top-bar">
|
||||
<div className="pswp__counter" />
|
||||
|
||||
<button
|
||||
className="pswp__button pswp__button--close"
|
||||
title="Share"
|
||||
/>
|
||||
<button
|
||||
className="pswp__button pswp__button--share"
|
||||
title="Share"
|
||||
/>
|
||||
<button
|
||||
className="pswp__button pswp__button--fs"
|
||||
title="Toggle fullscreen"
|
||||
/>
|
||||
<button className="pswp__button pswp__button--zoom" title="Zoom in/out" />
|
||||
<FavButton size={44} isClick={isFav} onClick={() => { onFavClick(photoSwipe?.currItem) }} />
|
||||
<div className="pswp__preloader">
|
||||
<div className="pswp__preloader__icn">
|
||||
<div className="pswp__preloader__cut">
|
||||
<div className="pswp__preloader__donut" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
|
||||
<div className="pswp__share-tooltip" />
|
||||
</div>
|
||||
<button
|
||||
className="pswp__button pswp__button--arrow--left"
|
||||
title="Previous (arrow left)"
|
||||
/>
|
||||
<button
|
||||
className="pswp__button pswp__button--arrow--right"
|
||||
title="Next (arrow right)"
|
||||
/>
|
||||
<div className="pswp__caption">
|
||||
<div className="pswp__caption__center" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PhotoSwipe;
|
19
src/components/PhotoSwipe/events.ts
Normal file
19
src/components/PhotoSwipe/events.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
export default [
|
||||
'beforeChange',
|
||||
'afterChange',
|
||||
'imageLoadComplete',
|
||||
'resize',
|
||||
'gettingData',
|
||||
'mouseUsed',
|
||||
'initialZoomIn',
|
||||
'initialZoomInEnd',
|
||||
'initialZoomOut',
|
||||
'initialZoomOutEnd',
|
||||
'parseVerticalMargin',
|
||||
'close',
|
||||
'unbindEvents',
|
||||
'destroy',
|
||||
'updateScrollOffset',
|
||||
'preventDragEvent',
|
||||
'shareLinkClick'
|
||||
];
|
|
@ -82,11 +82,28 @@ const GlobalStyles = createGlobalStyle`
|
|||
}
|
||||
.modal-90w{
|
||||
width:90vw;
|
||||
max-width:880px!important;
|
||||
max-width:880px!important;
|
||||
}
|
||||
.modal .modal-header, .modal .modal-footer {
|
||||
border-color: #444 !important;
|
||||
}
|
||||
.modal .modal-header .close {
|
||||
color: #aaa;
|
||||
text-shadow: none;
|
||||
}
|
||||
.modal .card {
|
||||
background-color: #303030;
|
||||
border: none;
|
||||
color: #aaa;
|
||||
}
|
||||
.modal .card > div {
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
.modal-content{
|
||||
background-color:#303030 !important;
|
||||
color:white;
|
||||
color:#aaa;
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -144,6 +161,7 @@ export default function App({ Component, pageProps }) {
|
|||
<FullScreenDropZone
|
||||
closeModal={closeUploadModal}
|
||||
showModal={showUploadModal}
|
||||
uploadModalView={uploadModalView}
|
||||
>
|
||||
<Head>
|
||||
<title>ente.io | Privacy friendly alternative to Google Photos</title>
|
||||
|
@ -168,7 +186,12 @@ export default function App({ Component, pageProps }) {
|
|||
</Spinner>
|
||||
</Container>
|
||||
) : (
|
||||
<Component uploadModalView={uploadModalView} showUploadModal={showUploadModal} closeUploadModal={closeUploadModal} setUploadButtonView={setUploadButtonView} />
|
||||
<Component
|
||||
uploadModalView={uploadModalView}
|
||||
showUploadModal={showUploadModal}
|
||||
closeUploadModal={closeUploadModal}
|
||||
setUploadButtonView={setUploadButtonView}
|
||||
/>
|
||||
)}
|
||||
</FullScreenDropZone>
|
||||
);
|
||||
|
|
55
src/pages/gallery/components/AddCollection.tsx
Normal file
55
src/pages/gallery/components/AddCollection.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
import React, { useState } from "react";
|
||||
import { Card } from "react-bootstrap";
|
||||
import styled from "styled-components";
|
||||
import CreateCollection from "./CreateCollection";
|
||||
import DropzoneWrapper from "./DropzoneWrapper";
|
||||
|
||||
const ImageContainer = styled.div`
|
||||
min-height: 192px;
|
||||
max-width: 192px;
|
||||
border: 1px solid #555;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 42px;
|
||||
`;
|
||||
|
||||
const StyledCard = styled(Card)`
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
export default function AddCollection(props) {
|
||||
|
||||
const [acceptedFiles, setAcceptedFiles] = useState<File[]>();
|
||||
const [createCollectionView, setCreateCollectionView] = useState(false);
|
||||
|
||||
const { closeUploadModal, showUploadModal, ...rest } = props;
|
||||
|
||||
const createCollection = (acceptedFiles) => {
|
||||
setAcceptedFiles(acceptedFiles);
|
||||
setCreateCollectionView(true);
|
||||
};
|
||||
const children = (
|
||||
<StyledCard>
|
||||
<ImageContainer>+</ImageContainer>
|
||||
<Card.Text style={{ textAlign: "center" }}>Create New Album</Card.Text>
|
||||
</StyledCard>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<DropzoneWrapper
|
||||
onDropAccepted={createCollection}
|
||||
onDropRejected={closeUploadModal}
|
||||
onDragOver={showUploadModal}
|
||||
children={children}
|
||||
/>
|
||||
<CreateCollection
|
||||
{...rest}
|
||||
modalView={createCollectionView}
|
||||
closeUploadModal={closeUploadModal}
|
||||
closeModal={() => setCreateCollectionView(false)}
|
||||
acceptedFiles={acceptedFiles}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,88 +1,37 @@
|
|||
import React from 'react';
|
||||
import Dropzone from 'react-dropzone';
|
||||
import styled from 'styled-components';
|
||||
import UploadService from 'services/uploadService';
|
||||
import { fetchData } from 'services/fileService';
|
||||
import { getToken } from 'utils/common/key';
|
||||
import DropzoneWrapper from './DropzoneWrapper';
|
||||
|
||||
const getColor = (props) => {
|
||||
if (props.isDragAccept) {
|
||||
return '#00e676';
|
||||
}
|
||||
if (props.isDragReject) {
|
||||
return '#ff1744';
|
||||
}
|
||||
if (props.isDragActive) {
|
||||
return '#2196f3';
|
||||
}
|
||||
};
|
||||
|
||||
const enableBorder = (props) => (props.isDragActive ? 'dashed' : 'none');
|
||||
|
||||
export const DropDiv = styled.div`
|
||||
width:200px;
|
||||
margin:5px;
|
||||
height:230px;
|
||||
color:black;
|
||||
border-width: 2px;
|
||||
border-radius: 2px;
|
||||
border-color: ${(props) => getColor(props)};
|
||||
border-style: ${(props) => enableBorder(props)};
|
||||
outline: none;
|
||||
transition: border 0.24s ease-in-out;
|
||||
`;
|
||||
|
||||
function CollectionDropZone({
|
||||
children,
|
||||
closeModal,
|
||||
setData,
|
||||
showModal,
|
||||
refetchData,
|
||||
collectionLatestFile,
|
||||
noDragEventsBubbling,
|
||||
setProgressView,
|
||||
token,
|
||||
encryptionKey,
|
||||
progressBarProps
|
||||
|
||||
}) {
|
||||
|
||||
const upload = async (acceptedFiles) => {
|
||||
const token = getToken();
|
||||
closeModal();
|
||||
progressBarProps.setPercentComplete(0);
|
||||
setProgressView(true);
|
||||
|
||||
await UploadService.uploadFiles(acceptedFiles, collectionLatestFile, token, progressBarProps);
|
||||
setData(await fetchData(token, encryptionKey, [collectionLatestFile.collection]));
|
||||
refetchData();
|
||||
setProgressView(false);
|
||||
}
|
||||
return (
|
||||
<Dropzone
|
||||
<DropzoneWrapper
|
||||
children={children}
|
||||
onDropAccepted={upload}
|
||||
onDragOver={showModal}
|
||||
onDropRejected={closeModal}
|
||||
noDragEventsBubbling
|
||||
accept="image/*, video/*, application/json, "
|
||||
>
|
||||
{({
|
||||
getRootProps,
|
||||
getInputProps,
|
||||
isDragActive,
|
||||
isDragAccept,
|
||||
isDragReject,
|
||||
}) => {
|
||||
return (
|
||||
<DropDiv
|
||||
{...getRootProps({
|
||||
isDragActive,
|
||||
isDragAccept,
|
||||
isDragReject,
|
||||
})}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
{children}
|
||||
</DropDiv>
|
||||
);
|
||||
}}
|
||||
</Dropzone>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,47 +1,35 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Card, Modal } from 'react-bootstrap';
|
||||
import { getActualKey } from 'utils/common/key';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import CollectionDropZone from './CollectionDropZone';
|
||||
import AddCollection from './AddCollection';
|
||||
import PreviewCard from './PreviewCard';
|
||||
import constants from 'utils/strings/constants';
|
||||
|
||||
function CollectionSelector(props) {
|
||||
const {
|
||||
uploadModalView,
|
||||
closeUploadModal,
|
||||
showUploadModal,
|
||||
collectionLatestFile,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
function CollectionSelector({
|
||||
uploadModalView,
|
||||
closeUploadModal,
|
||||
showUploadModal,
|
||||
collectionLatestFile,
|
||||
setProgressView,
|
||||
setData,
|
||||
progressBarProps,
|
||||
}) {
|
||||
|
||||
const [token, setToken] = useState(null);
|
||||
const [encryptionKey, setEncryptionKey] = useState(null);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setToken(getData(LS_KEYS.USER)?.token);
|
||||
setEncryptionKey(await getActualKey());
|
||||
})();
|
||||
});
|
||||
const CollectionIcons = collectionLatestFile?.map((item) => (
|
||||
<CollectionDropZone key={item.collectionID}
|
||||
<CollectionDropZone key={item.collection.id}
|
||||
{...rest}
|
||||
closeModal={closeUploadModal}
|
||||
showModal={showUploadModal}
|
||||
collectionLatestFile={item}
|
||||
noDragEventsBubbling
|
||||
setProgressView={setProgressView}
|
||||
token={token}
|
||||
encryptionKey={encryptionKey}
|
||||
setData={setData}
|
||||
progressBarProps={progressBarProps}
|
||||
>
|
||||
<Card>
|
||||
<PreviewCard data={item.file} updateUrl={() => { }} onClick={() => { }} />
|
||||
<Card.Text style={{ textAlign: 'center' }}>{item.collection.name}</Card.Text>
|
||||
<PreviewCard data={item.file} updateUrl={() => { }} forcedEnable />
|
||||
<Card.Text className="text-center">{item.collection.name}</Card.Text>
|
||||
</Card>
|
||||
|
||||
</CollectionDropZone>
|
||||
));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={uploadModalView}
|
||||
|
@ -50,15 +38,17 @@ function CollectionSelector({
|
|||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title >
|
||||
Select/Click on Collection to upload
|
||||
</Modal.Title>
|
||||
{constants.SELECT_COLLECTION}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body style={{ display: "flex", justifyContent: "space-around", flexWrap: "wrap" }}>
|
||||
<Modal.Body style={{ display: "flex", justifyContent: "flex-start", flexWrap: "wrap" }}>
|
||||
<AddCollection
|
||||
{...rest}
|
||||
showUploadModal={showUploadModal}
|
||||
closeUploadModal={closeUploadModal}
|
||||
/>
|
||||
{CollectionIcons}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={closeUploadModal}>Close</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { collection } from 'services/fileService';
|
||||
import { collection } from 'services/collectionService';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface CollectionProps {
|
||||
|
@ -57,6 +57,7 @@ export default function Collections(props: CollectionProps) {
|
|||
<Wrapper>
|
||||
<Chip active={!selected} onClick={clickHandler()}>All</Chip>
|
||||
{collections?.map(item => <Chip
|
||||
key={item.id}
|
||||
active={selected === item.id.toString()}
|
||||
onClick={clickHandler(item.id)}
|
||||
>{item.name}</Chip>)}
|
||||
|
|
72
src/pages/gallery/components/CreateCollection.tsx
Normal file
72
src/pages/gallery/components/CreateCollection.tsx
Normal file
|
@ -0,0 +1,72 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Form, Modal } from 'react-bootstrap';
|
||||
import { createAlbum } from 'services/collectionService';
|
||||
import UploadService from 'services/uploadService';
|
||||
import { collectionLatestFile } from 'services/collectionService'
|
||||
import { getToken } from 'utils/common/key';
|
||||
|
||||
export default function CreateCollection(props) {
|
||||
|
||||
const { acceptedFiles, setProgressView, progressBarProps, refetchData, modalView, closeModal, closeUploadModal } = props;
|
||||
const [albumName, setAlbumName] = useState("");
|
||||
|
||||
const handleChange = (event) => { setAlbumName(event.target.value); }
|
||||
|
||||
useEffect(() => {
|
||||
if (acceptedFiles == null)
|
||||
return;
|
||||
let commonPathPrefix: string = (() => {
|
||||
const paths: string[] = acceptedFiles.map(files => files.path);
|
||||
paths.sort();
|
||||
let firstPath = paths[0], lastPath = paths[paths.length - 1], L = firstPath.length, i = 0;
|
||||
while (i < L && firstPath.charAt(i) === lastPath.charAt(i)) i++;
|
||||
return firstPath.substring(0, i);
|
||||
})();
|
||||
if (commonPathPrefix)
|
||||
commonPathPrefix = commonPathPrefix.substr(1, commonPathPrefix.lastIndexOf('/') - 1);
|
||||
setAlbumName(commonPathPrefix);
|
||||
}, [acceptedFiles]);
|
||||
const handleSubmit = async (event) => {
|
||||
const token = getToken();
|
||||
event.preventDefault();
|
||||
|
||||
closeModal();
|
||||
closeUploadModal();
|
||||
|
||||
const collection = await createAlbum(albumName);
|
||||
|
||||
const collectionLatestFile: collectionLatestFile = { collection, file: null }
|
||||
|
||||
progressBarProps.setPercentComplete(0);
|
||||
setProgressView(true);
|
||||
|
||||
await UploadService.uploadFiles(acceptedFiles, collectionLatestFile, token, progressBarProps);
|
||||
refetchData();
|
||||
setProgressView(false);
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
show={modalView}
|
||||
onHide={closeModal}
|
||||
centered
|
||||
backdrop="static"
|
||||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
Create Collection
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="formBasicEmail">
|
||||
<Form.Label>Album Name:</Form.Label>
|
||||
<Form.Control type="text" placeholder="Enter Album Name" value={albumName} onChange={handleChange} />
|
||||
</Form.Group>
|
||||
<Button variant="primary" type="submit" style={{ width: "100%" }}>
|
||||
Submit
|
||||
</Button>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
64
src/pages/gallery/components/DropzoneWrapper.tsx
Normal file
64
src/pages/gallery/components/DropzoneWrapper.tsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
import React from 'react';
|
||||
import Dropzone from 'react-dropzone';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const getColor = (props) => {
|
||||
if (props.isDragAccept) {
|
||||
return '#00e676';
|
||||
}
|
||||
if (props.isDragReject) {
|
||||
return '#ff1744';
|
||||
}
|
||||
if (props.isDragActive) {
|
||||
return '#2196f3';
|
||||
}
|
||||
};
|
||||
|
||||
export const enableBorder = (props) => (props.isDragActive ? 'dashed' : 'none');
|
||||
|
||||
export const DropDiv = styled.div`
|
||||
width:200px;
|
||||
margin:5px;
|
||||
height:230px;
|
||||
color:black;
|
||||
border-width: 2px;
|
||||
border-radius: 2px;
|
||||
border-color: ${(props) => getColor(props)};
|
||||
border-style: ${(props) => enableBorder(props)};
|
||||
outline: none;
|
||||
transition: border 0.24s ease-in-out;
|
||||
`;
|
||||
|
||||
export function DropzoneWrapper(props) {
|
||||
const { children, ...callbackProps } = props
|
||||
return (
|
||||
<Dropzone
|
||||
noDragEventsBubbling
|
||||
accept="image/*, video/*, application/json, "
|
||||
{...callbackProps}
|
||||
>
|
||||
{({
|
||||
getRootProps,
|
||||
getInputProps,
|
||||
isDragActive,
|
||||
isDragAccept,
|
||||
isDragReject,
|
||||
}) => {
|
||||
return (
|
||||
<DropDiv
|
||||
{...getRootProps({
|
||||
isDragActive,
|
||||
isDragAccept,
|
||||
isDragReject,
|
||||
})}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
{children}
|
||||
</DropDiv>
|
||||
);
|
||||
}}
|
||||
</Dropzone>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropzoneWrapper;
|
|
@ -7,7 +7,8 @@ import PlayCircleOutline from 'components/PlayCircleOutline';
|
|||
interface IProps {
|
||||
data: file,
|
||||
updateUrl: (url: string) => void,
|
||||
onClick: () => void,
|
||||
onClick?: () => void,
|
||||
forcedEnable?: boolean,
|
||||
}
|
||||
|
||||
const Cont = styled.div<{ disabled: boolean }>`
|
||||
|
@ -41,7 +42,7 @@ const Cont = styled.div<{ disabled: boolean }>`
|
|||
|
||||
export default function PreviewCard(props: IProps) {
|
||||
const [imgSrc, setImgSrc] = useState<string>();
|
||||
const { data, onClick, updateUrl } = props;
|
||||
const { data, onClick, updateUrl, forcedEnable } = props;
|
||||
|
||||
useEffect(() => {
|
||||
if (data && !data.msrc) {
|
||||
|
@ -57,12 +58,12 @@ export default function PreviewCard(props: IProps) {
|
|||
}, [data]);
|
||||
|
||||
const handleClick = () => {
|
||||
if (data.msrc || imgSrc) {
|
||||
onClick();
|
||||
if (data?.msrc || imgSrc) {
|
||||
onClick?.();
|
||||
}
|
||||
}
|
||||
|
||||
return <Cont onClick={handleClick} disabled={!data?.msrc && !imgSrc}>
|
||||
return <Cont onClick={handleClick} disabled={!forcedEnable && !data?.msrc && !imgSrc}>
|
||||
<img src={data?.msrc || imgSrc} />
|
||||
{data?.metadata.fileType === 1 && <PlayCircleOutline />}
|
||||
</Cont>;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import CollectionSelector from 'pages/gallery/components/CollectionSelector';
|
||||
import React, { useRef } from 'react';
|
||||
import React from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
|
||||
function UploadButton({ showModal }) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { Alert, Modal, ProgressBar } from 'react-bootstrap';
|
||||
import constants from 'utils/strings/constants'
|
||||
|
||||
export default function UploadProgress({ fileCounter, uploadStage, now, ...props }) {
|
||||
return (
|
||||
|
@ -17,10 +18,10 @@ export default function UploadProgress({ fileCounter, uploadStage, now, ...props
|
|||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{now === 100 ? (
|
||||
<Alert variant='success'>Upload Completed</Alert>
|
||||
<Alert variant='success'>{constants.UPLOAD[3]}</Alert>
|
||||
) : (
|
||||
<>
|
||||
<Alert variant='info'>{uploadStage} {fileCounter?.current} of {fileCounter?.total}</Alert>
|
||||
<Alert variant='info'>{constants.UPLOAD[uploadStage]} {fileCounter?.total != 0 ? `${fileCounter?.current} ${constants.OF} ${fileCounter?.total}` : ''}</Alert>
|
||||
<ProgressBar animated now={now} />
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -3,31 +3,28 @@ import { useRouter } from 'next/router';
|
|||
import Spinner from 'react-bootstrap/Spinner';
|
||||
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
|
||||
import {
|
||||
collection,
|
||||
fetchCollections,
|
||||
file,
|
||||
getCollectionLatestFile,
|
||||
getFile,
|
||||
getFiles,
|
||||
getPreview,
|
||||
collectionLatestFile,
|
||||
fetchData,
|
||||
file,
|
||||
getFile,
|
||||
getPreview,
|
||||
fetchData,
|
||||
} from 'services/fileService';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import PreviewCard from './components/PreviewCard';
|
||||
import { getActualKey } from 'utils/common/key';
|
||||
import styled from 'styled-components';
|
||||
import { PhotoSwipe } from 'react-photoswipe';
|
||||
import PhotoSwipe from 'components/PhotoSwipe/PhotoSwipe';
|
||||
import { Options } from 'photoswipe';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { VariableSizeList as List } from 'react-window';
|
||||
import Collections from './components/Collections';
|
||||
import SadFace from 'components/SadFace';
|
||||
import Upload from './components/Upload';
|
||||
import { collection, fetchCollections, collectionLatestFile, getCollectionLatestFile, getFavItemIds } from 'services/collectionService';
|
||||
import constants from 'utils/strings/constants';
|
||||
|
||||
enum ITEM_TYPE {
|
||||
TIME = 'TIME',
|
||||
TILE = 'TILE',
|
||||
TIME = 'TIME',
|
||||
TILE = 'TILE',
|
||||
}
|
||||
export enum FILE_TYPE {
|
||||
IMAGE,
|
||||
|
@ -36,123 +33,124 @@ export enum FILE_TYPE {
|
|||
}
|
||||
|
||||
interface TimeStampListItem {
|
||||
itemType: ITEM_TYPE;
|
||||
items?: file[];
|
||||
itemStartIndex?: number;
|
||||
date?: string;
|
||||
itemType: ITEM_TYPE;
|
||||
items?: file[];
|
||||
itemStartIndex?: number;
|
||||
date?: string;
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: block;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 auto;
|
||||
|
||||
.pswp-thumbnail {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pswp-thumbnail {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
const ListItem = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const DeadCenter = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const ListContainer = styled.div`
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
color: #fff;
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
width: 1000px;
|
||||
}
|
||||
@media (min-width: 1000px) {
|
||||
width: 1000px;
|
||||
}
|
||||
|
||||
@media (min-width: 450px) and (max-width: 1000px) {
|
||||
width: 600px;
|
||||
}
|
||||
@media (min-width: 450px) and (max-width: 1000px) {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
@media (max-width: 450px) {
|
||||
width: 100%;
|
||||
}
|
||||
@media (max-width: 450px) {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const DateContainer = styled.div`
|
||||
padding: 0 4px;
|
||||
padding: 0 4px;
|
||||
`;
|
||||
|
||||
const PAGE_SIZE = 12;
|
||||
const COLUMNS = 3;
|
||||
|
||||
export default function Gallery(props) {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [collections, setCollections] = useState<collection[]>([]);
|
||||
const [collectionLatestFile, setCollectionLatestFile] = useState<
|
||||
collectionLatestFile[]
|
||||
>([]);
|
||||
const [data, setData] = useState<file[]>();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [options, setOptions] = useState<Options>({
|
||||
history: false,
|
||||
maxSpreadZoom: 5,
|
||||
});
|
||||
const fetching: { [k: number]: boolean } = {};
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [reload, setReload] = useState(0);
|
||||
const [collections, setCollections] = useState<collection[]>([]);
|
||||
const [collectionLatestFile, setCollectionLatestFile] = useState<
|
||||
collectionLatestFile[]
|
||||
>([]);
|
||||
const [data, setData] = useState<file[]>();
|
||||
const [favItemIds, setFavItemIds] = useState<Set<number>>();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [options, setOptions] = useState<Options>({
|
||||
history: false,
|
||||
maxSpreadZoom: 5,
|
||||
});
|
||||
const fetching: { [k: number]: boolean } = {};
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
||||
const token = getData(LS_KEYS.USER).token;
|
||||
if (!key) {
|
||||
router.push('/');
|
||||
useEffect(() => {
|
||||
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
||||
if (!key) {
|
||||
router.push('/');
|
||||
}
|
||||
const main = async () => {
|
||||
setLoading(true);
|
||||
await syncWithRemote();
|
||||
setLoading(false);
|
||||
};
|
||||
main();
|
||||
props.setUploadButtonView(true);
|
||||
}, []);
|
||||
|
||||
const syncWithRemote = async () => {
|
||||
const token = getData(LS_KEYS.USER).token;
|
||||
const encryptionKey = await getActualKey();
|
||||
const collections = await fetchCollections(token, encryptionKey);
|
||||
const data = await fetchData(token, collections);
|
||||
const collectionLatestFile = await getCollectionLatestFile(collections, data);
|
||||
const favItemIds = await getFavItemIds(data);
|
||||
setCollections(collections);
|
||||
setData(data);
|
||||
setCollectionLatestFile(collectionLatestFile);
|
||||
setFavItemIds(favItemIds);
|
||||
}
|
||||
if (!data || loading) {
|
||||
return (
|
||||
<div className='text-center'>
|
||||
<Spinner animation='border' variant='primary' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const main = async () => {
|
||||
setLoading(true);
|
||||
const encryptionKey = await getActualKey();
|
||||
const collections = await fetchCollections(token, encryptionKey);
|
||||
const data= await fetchData(token,encryptionKey,collections);
|
||||
setLoading(false);
|
||||
setCollections(collections);
|
||||
setData(data);
|
||||
const collectionLatestFile = await getCollectionLatestFile(
|
||||
collections,
|
||||
data
|
||||
);
|
||||
setCollectionLatestFile(collectionLatestFile);
|
||||
};
|
||||
main();
|
||||
props.setUploadButtonView(true);
|
||||
}, []);
|
||||
|
||||
if (!data || loading) {
|
||||
return (
|
||||
<div className='text-center'>
|
||||
<Spinner animation='border' variant='primary' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const updateUrl = (index: number) => (url: string) => {
|
||||
data[index] = {
|
||||
...data[index],
|
||||
msrc: url,
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
};
|
||||
if (data[index].metadata.fileType === FILE_TYPE.VIDEO && !data[index].html) {
|
||||
data[index].html = `
|
||||
const updateUrl = (index: number) => (url: string) => {
|
||||
data[index] = {
|
||||
...data[index],
|
||||
msrc: url,
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
};
|
||||
if (data[index].metadata.fileType === FILE_TYPE.VIDEO && !data[index].html) {
|
||||
data[index].html = `
|
||||
<div class="video-loading">
|
||||
<img src="${url}" />
|
||||
<div class="spinner-border text-light" role="status">
|
||||
|
@ -160,255 +158,258 @@ export default function Gallery(props) {
|
|||
</div>
|
||||
</div>
|
||||
`;
|
||||
delete data[index].src;
|
||||
}
|
||||
if (data[index].metadata.fileType === FILE_TYPE.IMAGE && !data[index].src) {
|
||||
data[index].src = url;
|
||||
}
|
||||
setData(data);
|
||||
};
|
||||
|
||||
const updateSrcUrl = (index: number, url: string) => {
|
||||
data[index] = {
|
||||
...data[index],
|
||||
src: url,
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
delete data[index].src;
|
||||
}
|
||||
if (data[index].metadata.fileType === FILE_TYPE.IMAGE && !data[index].src) {
|
||||
data[index].src = url;
|
||||
}
|
||||
setData(data);
|
||||
};
|
||||
if (data[index].metadata.fileType === FILE_TYPE.VIDEO) {
|
||||
data[index].html = `
|
||||
|
||||
const updateSrcUrl = (index: number, url: string) => {
|
||||
data[index] = {
|
||||
...data[index],
|
||||
src: url,
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
};
|
||||
if (data[index].metadata.fileType === FILE_TYPE.VIDEO) {
|
||||
data[index].html = `
|
||||
<video controls>
|
||||
<source src="${url}" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
`;
|
||||
delete data[index].src;
|
||||
}
|
||||
setData(data);
|
||||
};
|
||||
delete data[index].src;
|
||||
}
|
||||
setData(data);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
// syncWithRemote();
|
||||
};
|
||||
|
||||
const onThumbnailClick = (index: number) => () => {
|
||||
setOptions({
|
||||
...options,
|
||||
index,
|
||||
});
|
||||
setOpen(true);
|
||||
};
|
||||
const onThumbnailClick = (index: number) => () => {
|
||||
setOptions({
|
||||
...options,
|
||||
index,
|
||||
});
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const getThumbnail = (file: file[], index: number) => {
|
||||
return (
|
||||
<PreviewCard
|
||||
key={`tile-${file[index].id}`}
|
||||
data={file[index]}
|
||||
updateUrl={updateUrl(file[index].dataIndex)}
|
||||
onClick={onThumbnailClick(index)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const getThumbnail = (file: file[], index: number) => {
|
||||
return (
|
||||
<PreviewCard
|
||||
key={`tile-${file[index].id}`}
|
||||
data={file[index]}
|
||||
updateUrl={updateUrl(file[index].dataIndex)}
|
||||
onClick={onThumbnailClick(index)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const getSlideData = async (instance: any, index: number, item: file) => {
|
||||
const token = getData(LS_KEYS.USER).token;
|
||||
if (!item.msrc) {
|
||||
const url = await getPreview(token, item);
|
||||
updateUrl(item.dataIndex)(url);
|
||||
item.msrc = url;
|
||||
if (!item.src) {
|
||||
item.src = url;
|
||||
}
|
||||
item.w = window.innerWidth;
|
||||
item.h = window.innerHeight;
|
||||
try {
|
||||
instance.invalidateCurrItems();
|
||||
instance.updateSize(true);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if ((!item.src || item.src === item.msrc) && !fetching[item.dataIndex]) {
|
||||
fetching[item.dataIndex] = true;
|
||||
const url = await getFile(token, item);
|
||||
updateSrcUrl(item.dataIndex, url);
|
||||
if (item.metadata.fileType === FILE_TYPE.VIDEO) {
|
||||
item.html = `
|
||||
const getSlideData = async (instance: any, index: number, item: file) => {
|
||||
const token = getData(LS_KEYS.USER).token;
|
||||
if (!item.msrc) {
|
||||
const url = await getPreview(token, item);
|
||||
updateUrl(item.dataIndex)(url);
|
||||
item.msrc = url;
|
||||
if (!item.src) {
|
||||
item.src = url;
|
||||
}
|
||||
item.w = window.innerWidth;
|
||||
item.h = window.innerHeight;
|
||||
try {
|
||||
instance.invalidateCurrItems();
|
||||
instance.updateSize(true);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if ((!item.src || item.src === item.msrc) && !fetching[item.dataIndex]) {
|
||||
fetching[item.dataIndex] = true;
|
||||
const url = await getFile(token, item);
|
||||
updateSrcUrl(item.dataIndex, url);
|
||||
if (item.metadata.fileType === FILE_TYPE.VIDEO) {
|
||||
item.html = `
|
||||
<video width="320" height="240" controls>
|
||||
<source src="${url}" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
`;
|
||||
delete item.src;
|
||||
item.w = window.innerWidth;
|
||||
} else {
|
||||
item.src = url;
|
||||
}
|
||||
item.h = window.innerHeight;
|
||||
try {
|
||||
instance.invalidateCurrItems();
|
||||
instance.updateSize(true);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const selectCollection = (id?: string) => {
|
||||
const href = `/gallery?collection=${id || ''}`;
|
||||
router.push(href, undefined, { shallow: true });
|
||||
};
|
||||
|
||||
let idSet = new Set();
|
||||
const filteredData = data
|
||||
.map((item, index) => ({
|
||||
...item,
|
||||
dataIndex: index,
|
||||
}))
|
||||
.filter((item) => {
|
||||
if (!idSet.has(item.id)) {
|
||||
if (
|
||||
!router.query.collection ||
|
||||
router.query.collection === item.collectionID.toString()
|
||||
) {
|
||||
idSet.add(item.id);
|
||||
return true;
|
||||
delete item.src;
|
||||
item.w = window.innerWidth;
|
||||
} else {
|
||||
item.src = url;
|
||||
}
|
||||
item.h = window.innerHeight;
|
||||
try {
|
||||
instance.invalidateCurrItems();
|
||||
instance.updateSize(true);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
const isSameDay = (first, second) => {
|
||||
return (
|
||||
first.getFullYear() === second.getFullYear() &&
|
||||
first.getMonth() === second.getMonth() &&
|
||||
first.getDate() === second.getDate()
|
||||
);
|
||||
};
|
||||
const selectCollection = (id?: string) => {
|
||||
const href = `/gallery?collection=${id || ''}`;
|
||||
router.push(href, undefined, { shallow: true });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collections
|
||||
collections={collections}
|
||||
selected={router.query.collection?.toString()}
|
||||
selectCollection={selectCollection}
|
||||
/>
|
||||
<Upload
|
||||
uploadModalView={props.uploadModalView}
|
||||
showUploadModal={props.showUploadModal}
|
||||
closeUploadModal={props.closeUploadModal}
|
||||
collectionLatestFile={collectionLatestFile}
|
||||
setData={setData}/>
|
||||
|
||||
{filteredData.length ? (
|
||||
<Container>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => {
|
||||
let columns;
|
||||
if (width >= 1000) {
|
||||
columns = 5;
|
||||
} else if (width < 1000 && width >= 450) {
|
||||
columns = 3;
|
||||
} else if (width < 450 && width >= 300) {
|
||||
columns = 2;
|
||||
} else {
|
||||
columns = 1;
|
||||
}
|
||||
|
||||
const timeStampList: TimeStampListItem[] = [];
|
||||
let listItemIndex = 0;
|
||||
let currentDate = -1;
|
||||
filteredData.forEach((item, index) => {
|
||||
let idSet = new Set();
|
||||
const filteredData = data
|
||||
.map((item, index) => ({
|
||||
...item,
|
||||
dataIndex: index,
|
||||
}))
|
||||
.filter((item) => {
|
||||
if (!idSet.has(item.id)) {
|
||||
if (
|
||||
!isSameDay(
|
||||
new Date(item.metadata.creationTime / 1000),
|
||||
new Date(currentDate)
|
||||
)
|
||||
!router.query.collection ||
|
||||
router.query.collection === item.collectionID.toString()
|
||||
) {
|
||||
currentDate = item.metadata.creationTime / 1000;
|
||||
const dateTimeFormat = new Intl.DateTimeFormat('en-IN', {
|
||||
weekday: 'short',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.TIME,
|
||||
date: dateTimeFormat.format(currentDate),
|
||||
});
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.TILE,
|
||||
items: [item],
|
||||
itemStartIndex: index,
|
||||
});
|
||||
listItemIndex = 1;
|
||||
} else {
|
||||
if (listItemIndex < columns) {
|
||||
timeStampList[timeStampList.length - 1].items.push(item);
|
||||
listItemIndex++;
|
||||
} else {
|
||||
listItemIndex = 1;
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.TILE,
|
||||
items: [item],
|
||||
itemStartIndex: index,
|
||||
});
|
||||
}
|
||||
idSet.add(item.id);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return (
|
||||
<List
|
||||
itemSize={(index) =>
|
||||
timeStampList[index].itemType === ITEM_TYPE.TIME
|
||||
? 30
|
||||
: 200
|
||||
}
|
||||
height={height}
|
||||
width={width}
|
||||
itemCount={timeStampList.length}
|
||||
key={`${router.query.collection}-${columns}`}
|
||||
>
|
||||
{({ index, style }) => {
|
||||
return (
|
||||
<ListItem style={style}>
|
||||
<ListContainer>
|
||||
{timeStampList[index].itemType ===
|
||||
ITEM_TYPE.TIME ? (
|
||||
<DateContainer>
|
||||
{timeStampList[index].date}
|
||||
</DateContainer>
|
||||
) : (
|
||||
timeStampList[index].items.map((item, idx) => {
|
||||
return getThumbnail(
|
||||
filteredData,
|
||||
timeStampList[index].itemStartIndex + idx
|
||||
);
|
||||
})
|
||||
)}
|
||||
</ListContainer>
|
||||
</ListItem>
|
||||
);
|
||||
}}
|
||||
</List>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
<PhotoSwipe
|
||||
isOpen={open}
|
||||
items={filteredData}
|
||||
options={options}
|
||||
onClose={handleClose}
|
||||
gettingData={getSlideData}
|
||||
/>
|
||||
</Container>
|
||||
) : (
|
||||
<DeadCenter>
|
||||
<SadFace height={100} width={100} />
|
||||
<div>No content found!</div>
|
||||
</DeadCenter>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
const isSameDay = (first, second) => {
|
||||
return (
|
||||
first.getFullYear() === second.getFullYear() &&
|
||||
first.getMonth() === second.getMonth() &&
|
||||
first.getDate() === second.getDate()
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collections
|
||||
collections={collections}
|
||||
selected={router.query.collection?.toString()}
|
||||
selectCollection={selectCollection}
|
||||
/>
|
||||
<Upload
|
||||
uploadModalView={props.uploadModalView}
|
||||
closeUploadModal={props.closeUploadModal}
|
||||
showUploadModal={props.showUploadModal}
|
||||
collectionLatestFile={collectionLatestFile}
|
||||
refetchData={syncWithRemote}
|
||||
|
||||
/>
|
||||
{filteredData.length ? (
|
||||
<Container>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => {
|
||||
let columns;
|
||||
if (width >= 1000) {
|
||||
columns = 5;
|
||||
} else if (width < 1000 && width >= 450) {
|
||||
columns = 3;
|
||||
} else if (width < 450 && width >= 300) {
|
||||
columns = 2;
|
||||
} else {
|
||||
columns = 1;
|
||||
}
|
||||
|
||||
const timeStampList: TimeStampListItem[] = [];
|
||||
let listItemIndex = 0;
|
||||
let currentDate = -1;
|
||||
filteredData.forEach((item, index) => {
|
||||
if (
|
||||
!isSameDay(
|
||||
new Date(item.metadata.creationTime / 1000),
|
||||
new Date(currentDate)
|
||||
)
|
||||
) {
|
||||
currentDate = item.metadata.creationTime / 1000;
|
||||
const dateTimeFormat = new Intl.DateTimeFormat('en-IN', {
|
||||
weekday: 'short',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.TIME,
|
||||
date: dateTimeFormat.format(currentDate),
|
||||
});
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.TILE,
|
||||
items: [item],
|
||||
itemStartIndex: index,
|
||||
});
|
||||
listItemIndex = 1;
|
||||
} else {
|
||||
if (listItemIndex < columns) {
|
||||
timeStampList[timeStampList.length - 1].items.push(item);
|
||||
listItemIndex++;
|
||||
} else {
|
||||
listItemIndex = 1;
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.TILE,
|
||||
items: [item],
|
||||
itemStartIndex: index,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<List
|
||||
itemSize={(index) =>
|
||||
timeStampList[index].itemType === ITEM_TYPE.TIME
|
||||
? 30
|
||||
: 200
|
||||
}
|
||||
height={height}
|
||||
width={width}
|
||||
itemCount={timeStampList.length}
|
||||
key={`${router.query.collection}-${columns}`}
|
||||
>
|
||||
{({ index, style }) => {
|
||||
return (
|
||||
<ListItem style={style}>
|
||||
<ListContainer>
|
||||
{timeStampList[index].itemType ===
|
||||
ITEM_TYPE.TIME ? (
|
||||
<DateContainer>
|
||||
{timeStampList[index].date}
|
||||
</DateContainer>
|
||||
) : (
|
||||
timeStampList[index].items.map((item, idx) => {
|
||||
return getThumbnail(
|
||||
filteredData,
|
||||
timeStampList[index].itemStartIndex + idx
|
||||
);
|
||||
})
|
||||
)}
|
||||
</ListContainer>
|
||||
</ListItem>
|
||||
);
|
||||
}}
|
||||
</List>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
<PhotoSwipe
|
||||
isOpen={open}
|
||||
items={filteredData}
|
||||
options={options}
|
||||
onClose={handleClose}
|
||||
gettingData={getSlideData}
|
||||
favItemIds={favItemIds}
|
||||
setFavItemIds={setFavItemIds}
|
||||
/>
|
||||
</Container>
|
||||
) : (
|
||||
<DeadCenter>
|
||||
<div>{constants.NOTHING_HERE}</div>
|
||||
</DeadCenter>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
235
src/services/collectionService.ts
Normal file
235
src/services/collectionService.ts
Normal file
|
@ -0,0 +1,235 @@
|
|||
import { getEndpoint } from "utils/common/apiUtil";
|
||||
import { getData, LS_KEYS } from "utils/storage/localStorage";
|
||||
import { file, user, getFiles } from "./fileService";
|
||||
import localForage from 'localforage';
|
||||
|
||||
import HTTPService from "./HTTPService";
|
||||
import * as Comlink from 'comlink';
|
||||
import { keyEncryptionResult } from "./uploadService";
|
||||
import { getActualKey, getToken } from "utils/common/key";
|
||||
|
||||
|
||||
const CryptoWorker: any =
|
||||
typeof window !== 'undefined' &&
|
||||
Comlink.wrap(new Worker('worker/crypto.worker.js', { type: 'module' }));
|
||||
const ENDPOINT = getEndpoint();
|
||||
|
||||
|
||||
enum CollectionType {
|
||||
folder = "folder",
|
||||
favorites = "favorites",
|
||||
album = "album",
|
||||
}
|
||||
|
||||
export interface collection {
|
||||
id: string;
|
||||
owner: user;
|
||||
key?: string;
|
||||
name?: string;
|
||||
encryptedName?: string;
|
||||
nameDecryptionNonce?: string;
|
||||
type: string;
|
||||
attributes: collectionAttributes
|
||||
sharees: user[];
|
||||
updationTime: number;
|
||||
encryptedKey: string;
|
||||
keyDecryptionNonce: string;
|
||||
isDeleted: boolean;
|
||||
}
|
||||
|
||||
interface collectionAttributes {
|
||||
encryptedPath?: string;
|
||||
pathDecryptionNonce?: string
|
||||
};
|
||||
|
||||
export interface collectionLatestFile {
|
||||
collection: collection
|
||||
file: file;
|
||||
}
|
||||
|
||||
|
||||
const getCollectionSecrets = async (collection: collection, masterKey: string) => {
|
||||
const worker = await new CryptoWorker();
|
||||
const userID = getData(LS_KEYS.USER).id;
|
||||
let decryptedKey: string;
|
||||
if (collection.owner.id == userID) {
|
||||
decryptedKey = await worker.decryptB64(
|
||||
collection.encryptedKey,
|
||||
collection.keyDecryptionNonce,
|
||||
masterKey
|
||||
);
|
||||
|
||||
} else {
|
||||
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
|
||||
const secretKey = await worker.decryptB64(
|
||||
keyAttributes.encryptedSecretKey,
|
||||
keyAttributes.secretKeyDecryptionNonce,
|
||||
masterKey
|
||||
);
|
||||
decryptedKey = await worker.boxSealOpen(
|
||||
collection.encryptedKey,
|
||||
keyAttributes.publicKey,
|
||||
secretKey
|
||||
);
|
||||
}
|
||||
collection.name = collection.name || await worker.decryptString(
|
||||
collection.encryptedName,
|
||||
collection.nameDecryptionNonce,
|
||||
decryptedKey);
|
||||
return {
|
||||
...collection,
|
||||
key: decryptedKey,
|
||||
};
|
||||
};
|
||||
|
||||
const getCollections = async (
|
||||
token: string,
|
||||
sinceTime: string,
|
||||
key: string
|
||||
): Promise<collection[]> => {
|
||||
try {
|
||||
const resp = await HTTPService.get(`${ENDPOINT}/collections`, {
|
||||
sinceTime: sinceTime,
|
||||
}, { 'X-Auth-Token': token, });
|
||||
const promises: Promise<collection>[] = resp.data.collections.map(
|
||||
(collection: collection) => getCollectionSecrets(collection, key)
|
||||
);
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
catch (e) {
|
||||
console.log("getCollections falied- " + e);
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchCollections = async (token: string, key: string) => {
|
||||
const collections = await getCollections(token, '0', key);
|
||||
const favCollection = collections.filter(collection => collection.type === CollectionType.favorites);
|
||||
await localForage.setItem('fav-collection', favCollection);
|
||||
return collections;
|
||||
};
|
||||
|
||||
export const getCollectionLatestFile = (
|
||||
collections: collection[],
|
||||
files: file[]
|
||||
): collectionLatestFile[] => {
|
||||
const latestFile = new Map<number, file>();
|
||||
const collectionMap = new Map<number, collection>();
|
||||
|
||||
collections.forEach(collection => collectionMap.set(Number(collection.id), collection));
|
||||
files.forEach(file => {
|
||||
if (!latestFile.has(file.collectionID)) {
|
||||
latestFile.set(file.collectionID, file)
|
||||
}
|
||||
});
|
||||
let allCollectionLatestFile: collectionLatestFile[] = [];
|
||||
for (const [collectionID, file] of latestFile) {
|
||||
allCollectionLatestFile.push({ collection: collectionMap.get(collectionID), file });
|
||||
}
|
||||
return allCollectionLatestFile;
|
||||
}
|
||||
|
||||
export const getFavItemIds = async (files: file[]): Promise<Set<number>> => {
|
||||
|
||||
let favCollection: collection = (await localForage.getItem<collection>('fav-collection'))[0];
|
||||
if (!favCollection)
|
||||
return new Set();
|
||||
|
||||
return new Set(files.filter(file => file.collectionID === Number(favCollection.id)).map((file): number => file.id));
|
||||
}
|
||||
|
||||
export const createAlbum = async (albumName: string) => {
|
||||
return AddCollection(albumName, CollectionType.album);
|
||||
}
|
||||
|
||||
|
||||
export const AddCollection = async (collectionName: string, type: CollectionType) => {
|
||||
const worker = await new CryptoWorker();
|
||||
const encryptionKey = await getActualKey();
|
||||
const token = getToken();
|
||||
const collectionKey: string = await worker.generateMasterKey();
|
||||
const { encryptedData: encryptedKey, nonce: keyDecryptionNonce }: keyEncryptionResult = await worker.encryptToB64(collectionKey, encryptionKey);
|
||||
const { encryptedData: encryptedName, nonce: nameDecryptionNonce }: keyEncryptionResult = await worker.encryptToB64(collectionName, collectionKey);
|
||||
const newCollection: collection = {
|
||||
id: null,
|
||||
owner: null,
|
||||
encryptedKey,
|
||||
keyDecryptionNonce,
|
||||
encryptedName,
|
||||
nameDecryptionNonce,
|
||||
type,
|
||||
attributes: {},
|
||||
sharees: null,
|
||||
updationTime: null,
|
||||
isDeleted: false
|
||||
};
|
||||
let createdCollection: collection = await createCollection(newCollection, token);
|
||||
createdCollection = await getCollectionSecrets(createdCollection, encryptionKey);
|
||||
return createdCollection;
|
||||
}
|
||||
|
||||
const createCollection = async (collectionData: collection, token: string): Promise<collection> => {
|
||||
try {
|
||||
const response = await HTTPService.post(`${ENDPOINT}/collections`, collectionData, null, { 'X-Auth-Token': token });
|
||||
return response.data.collection;
|
||||
} catch (e) {
|
||||
console.log("create Collection failed " + e);
|
||||
}
|
||||
}
|
||||
|
||||
export const addToFavorites = async (file: file) => {
|
||||
let favCollection: collection = (await localForage.getItem<collection>('fav-collection'))[0];
|
||||
if (!favCollection) {
|
||||
favCollection = await AddCollection("Favorites", CollectionType.favorites);
|
||||
await localForage.setItem('fav-collection', favCollection);
|
||||
}
|
||||
await addtoCollection(favCollection, [file])
|
||||
}
|
||||
|
||||
export const removeFromFavorites = async (file: file) => {
|
||||
let favCollection: collection = (await localForage.getItem<collection>('fav-collection'))[0];
|
||||
await removeFromCollection(favCollection, [file])
|
||||
}
|
||||
|
||||
const addtoCollection = async (collection: collection, files: file[]) => {
|
||||
try {
|
||||
const params = new Object();
|
||||
const worker = await new CryptoWorker();
|
||||
const token = getToken();
|
||||
params["collectionID"] = collection.id;
|
||||
await Promise.all(files.map(async file => {
|
||||
file.collectionID = Number(collection.id);
|
||||
const newEncryptedKey: keyEncryptionResult = await worker.encryptToB64(file.key, collection.key);
|
||||
file.encryptedKey = newEncryptedKey.encryptedData;
|
||||
file.keyDecryptionNonce = newEncryptedKey.nonce;
|
||||
if (params["files"] == undefined) {
|
||||
params["files"] = [];
|
||||
}
|
||||
params["files"].push({
|
||||
id: file.id,
|
||||
encryptedKey: file.encryptedKey,
|
||||
keyDecryptionNonce: file.keyDecryptionNonce
|
||||
})
|
||||
return file;
|
||||
}));
|
||||
await HTTPService.post(`${ENDPOINT}/collections/add-files`, params, null, { 'X-Auth-Token': token });
|
||||
} catch (e) {
|
||||
console.log("Add to collection Failed " + e);
|
||||
}
|
||||
}
|
||||
const removeFromCollection = async (collection: collection, files: file[]) => {
|
||||
try {
|
||||
const params = new Object();
|
||||
const token = getToken();
|
||||
params["collectionID"] = collection.id;
|
||||
await Promise.all(files.map(async file => {
|
||||
if (params["fileIDs"] == undefined) {
|
||||
params["fileIDs"] = [];
|
||||
}
|
||||
params["fileIDs"].push(file.id);
|
||||
}));
|
||||
await HTTPService.post(`${ENDPOINT}/collections/remove-files`, params, null, { 'X-Auth-Token': token });
|
||||
} catch (e) {
|
||||
console.log("remove from collection failed " + e);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { getEndpoint } from 'utils/common/apiUtil';
|
||||
import HTTPService from './HTTPService';
|
||||
import * as Comlink from 'comlink';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import localForage from 'localforage';
|
||||
import { collection } from './collectionService';
|
||||
|
||||
const CryptoWorker: any =
|
||||
typeof window !== 'undefined' &&
|
||||
|
@ -29,17 +29,6 @@ export interface user {
|
|||
email: string;
|
||||
}
|
||||
|
||||
export interface collection {
|
||||
id: string;
|
||||
owner: user;
|
||||
key: string;
|
||||
name: string;
|
||||
type: string;
|
||||
creationTime: number;
|
||||
encryptedKey: string;
|
||||
keyDecryptionNonce: string;
|
||||
isDeleted: boolean;
|
||||
}
|
||||
|
||||
export interface file {
|
||||
id: number;
|
||||
|
@ -57,69 +46,14 @@ export interface file {
|
|||
h: number;
|
||||
isDeleted: boolean;
|
||||
dataIndex: number;
|
||||
updationTime: number;
|
||||
}
|
||||
|
||||
export interface collectionLatestFile {
|
||||
collection: collection
|
||||
file: file;
|
||||
}
|
||||
|
||||
const getCollectionKey = async (collection: collection, key: Uint8Array) => {
|
||||
const worker = await new CryptoWorker();
|
||||
const userID = getData(LS_KEYS.USER).id;
|
||||
var decryptedKey;
|
||||
if (collection.owner.id == userID) {
|
||||
decryptedKey = await worker.decrypt(
|
||||
await worker.fromB64(collection.encryptedKey),
|
||||
await worker.fromB64(collection.keyDecryptionNonce),
|
||||
key
|
||||
);
|
||||
} else {
|
||||
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
|
||||
const secretKey = await worker.decrypt(
|
||||
await worker.fromB64(keyAttributes.encryptedSecretKey),
|
||||
await worker.fromB64(keyAttributes.secretKeyDecryptionNonce),
|
||||
key
|
||||
);
|
||||
decryptedKey = await worker.boxSealOpen(
|
||||
await worker.fromB64(collection.encryptedKey),
|
||||
await worker.fromB64(keyAttributes.publicKey),
|
||||
secretKey
|
||||
);
|
||||
}
|
||||
return {
|
||||
...collection,
|
||||
key: decryptedKey,
|
||||
};
|
||||
};
|
||||
|
||||
const getCollections = async (
|
||||
token: string,
|
||||
sinceTime: string,
|
||||
key: Uint8Array
|
||||
): Promise<collection[]> => {
|
||||
const resp = await HTTPService.get(`${ENDPOINT}/collections`, {
|
||||
token: token,
|
||||
sinceTime: sinceTime,
|
||||
});
|
||||
const ignore: Set<number> = new Set([206, 208, 209]);
|
||||
const promises: Promise<collection>[] = resp.data.collections.filter(collection => !ignore.has(collection.id)).map(
|
||||
(collection: collection) => getCollectionKey(collection, key)
|
||||
);
|
||||
return await Promise.all(promises);
|
||||
};
|
||||
|
||||
export const fetchCollections = async (token: string, key: string) => {
|
||||
const worker = await new CryptoWorker();
|
||||
return getCollections(token, '0', await worker.fromB64(key));
|
||||
};
|
||||
|
||||
export const fetchData = async (token, encryptionKey, collections) => {
|
||||
const resp = await getFiles(
|
||||
'0',
|
||||
export const fetchData = async (token, collections) => {
|
||||
const resp = await fetchFiles(
|
||||
token,
|
||||
'100',
|
||||
encryptionKey,
|
||||
collections
|
||||
);
|
||||
|
||||
|
@ -132,125 +66,129 @@ export const fetchData = async (token, encryptionKey, collections) => {
|
|||
);
|
||||
}
|
||||
|
||||
export const getFiles = async (
|
||||
sinceTime: string,
|
||||
export const fetchFiles = async (
|
||||
token: string,
|
||||
limit: string,
|
||||
key: string,
|
||||
collections: collection[]
|
||||
) => {
|
||||
const worker = await new CryptoWorker();
|
||||
let files: Array<file> = (await localForage.getItem<file[]>('files')) || [];
|
||||
for (const index in collections) {
|
||||
const collection = collections[index];
|
||||
if (collection.isDeleted) {
|
||||
// TODO: Remove files in this collection from localForage and cache
|
||||
continue;
|
||||
const fetchedFiles = await getFiles(collections, null, "100", token);
|
||||
|
||||
files.push(...fetchedFiles);
|
||||
var latestFiles = new Map<string, file>();
|
||||
files.forEach((file) => {
|
||||
let uid = `${file.collectionID}-${file.id}`;
|
||||
if (!latestFiles.has(uid) || latestFiles.get(uid).updationTime < file.updationTime) {
|
||||
latestFiles.set(uid, file);
|
||||
}
|
||||
let time =
|
||||
(await localForage.getItem<string>(`${collection.id}-time`)) || sinceTime;
|
||||
let resp;
|
||||
do {
|
||||
resp = await HTTPService.get(`${ENDPOINT}/collections/diff`, {
|
||||
collectionID: collection.id,
|
||||
sinceTime: time,
|
||||
token,
|
||||
limit,
|
||||
});
|
||||
const promises: Promise<file>[] = resp.data.diff.filter(file => !file.isDeleted).map(
|
||||
async (file: file) => {
|
||||
console.log(file);
|
||||
file.key = await worker.decryptB64(
|
||||
file.encryptedKey,
|
||||
file.keyDecryptionNonce,
|
||||
collection.key
|
||||
);
|
||||
file.metadata = await worker.decryptMetadata(file);
|
||||
return file;
|
||||
}
|
||||
);
|
||||
files.push(...(await Promise.all(promises)));
|
||||
files = files.sort(
|
||||
(a, b) => b.metadata.creationTime - a.metadata.creationTime
|
||||
);
|
||||
if (resp.data.diff.length) {
|
||||
time = resp.data.diff.slice(-1)[0].updationTime.toString();
|
||||
}
|
||||
} while (resp.data.diff.length);
|
||||
await localForage.setItem(`${collection.id}-time`, time);
|
||||
});
|
||||
files = [];
|
||||
for (const [_, file] of latestFiles.entries()) {
|
||||
if (!file.isDeleted)
|
||||
files.push(file);
|
||||
}
|
||||
files = files.sort(
|
||||
(a, b) => b.metadata.creationTime - a.metadata.creationTime
|
||||
);
|
||||
await localForage.setItem('files', files);
|
||||
return files;
|
||||
};
|
||||
|
||||
export const getPreview = async (token: string, file: file) => {
|
||||
const cache = await caches.open('thumbs');
|
||||
const cacheResp: Response = await cache.match(file.id.toString());
|
||||
if (cacheResp) {
|
||||
return URL.createObjectURL(await cacheResp.blob());
|
||||
}
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/files/preview/${file.id}`,
|
||||
{ token },
|
||||
null,
|
||||
{ responseType: 'arraybuffer' }
|
||||
);
|
||||
const worker = await new CryptoWorker();
|
||||
const decrypted: any = await worker.decryptThumbnail(
|
||||
new Uint8Array(resp.data),
|
||||
await worker.fromB64(file.thumbnail.decryptionHeader),
|
||||
file.key
|
||||
);
|
||||
export const getFiles = async (collections: collection[], sinceTime: string, limit: string, token: string): Promise<file[]> => {
|
||||
try {
|
||||
await cache.put(file.id.toString(), new Response(new Blob([decrypted])));
|
||||
const worker = await new CryptoWorker();
|
||||
let promises: Promise<file>[] = [];
|
||||
for (const index in collections) {
|
||||
const collection = collections[index];
|
||||
if (collection.isDeleted) {
|
||||
// TODO: Remove files in this collection from localForage and cache
|
||||
continue;
|
||||
}
|
||||
let time =
|
||||
sinceTime || (await localForage.getItem<string>(`${collection.id}-time`)) || "0";
|
||||
let resp;
|
||||
do {
|
||||
resp = await HTTPService.get(`${ENDPOINT}/collections/diff`, {
|
||||
collectionID: collection.id,
|
||||
sinceTime: time,
|
||||
limit,
|
||||
},
|
||||
{
|
||||
'X-Auth-Token': token
|
||||
});
|
||||
promises.push(...resp.data.diff.map(
|
||||
async (file: file) => {
|
||||
if (!file.isDeleted) {
|
||||
|
||||
file.key = await worker.decryptB64(
|
||||
file.encryptedKey,
|
||||
file.keyDecryptionNonce,
|
||||
collection.key
|
||||
);
|
||||
file.metadata = await worker.decryptMetadata(file);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
));
|
||||
|
||||
if (resp.data.diff.length) {
|
||||
time = resp.data.diff.slice(-1)[0].updationTime.toString();
|
||||
}
|
||||
} while (resp.data.diff.length);
|
||||
await localForage.setItem(`${collection.id}-time`, time);
|
||||
}
|
||||
return Promise.all(promises);
|
||||
} catch (e) {
|
||||
// TODO: handle storage full exception.
|
||||
console.log("Get files failed" + e);
|
||||
}
|
||||
}
|
||||
export const getPreview = async (token: string, file: file) => {
|
||||
try {
|
||||
const cache = await caches.open('thumbs');
|
||||
const cacheResp: Response = await cache.match(file.id.toString());
|
||||
if (cacheResp) {
|
||||
return URL.createObjectURL(await cacheResp.blob());
|
||||
}
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/files/preview/${file.id}`,
|
||||
null,
|
||||
{ 'X-Auth-Token': token },
|
||||
{ responseType: 'arraybuffer' }
|
||||
);
|
||||
const worker = await new CryptoWorker();
|
||||
const decrypted: any = await worker.decryptThumbnail(
|
||||
new Uint8Array(resp.data),
|
||||
await worker.fromB64(file.thumbnail.decryptionHeader),
|
||||
file.key
|
||||
);
|
||||
try {
|
||||
await cache.put(file.id.toString(), new Response(new Blob([decrypted])));
|
||||
} catch (e) {
|
||||
// TODO: handle storage full exception.
|
||||
}
|
||||
return URL.createObjectURL(new Blob([decrypted]));
|
||||
} catch (e) {
|
||||
console.log("get preview Failed" + e);
|
||||
}
|
||||
return URL.createObjectURL(new Blob([decrypted]));
|
||||
};
|
||||
|
||||
export const getFile = async (token: string, file: file) => {
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/files/download/${file.id}`,
|
||||
{ token },
|
||||
null,
|
||||
{ responseType: 'arraybuffer' }
|
||||
);
|
||||
const worker = await new CryptoWorker();
|
||||
const decrypted: any = await worker.decryptFile(
|
||||
new Uint8Array(resp.data),
|
||||
await worker.fromB64(file.file.decryptionHeader),
|
||||
file.key
|
||||
);
|
||||
return URL.createObjectURL(new Blob([decrypted]));
|
||||
try {
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/files/download/${file.id}`,
|
||||
null,
|
||||
{ 'X-Auth-Token': token },
|
||||
{ responseType: 'arraybuffer' }
|
||||
);
|
||||
const worker = await new CryptoWorker();
|
||||
const decrypted: any = await worker.decryptFile(
|
||||
new Uint8Array(resp.data),
|
||||
await worker.fromB64(file.file.decryptionHeader),
|
||||
file.key
|
||||
);
|
||||
return URL.createObjectURL(new Blob([decrypted]));
|
||||
}
|
||||
catch (e) {
|
||||
console.log("get file failed " + e);
|
||||
}
|
||||
};
|
||||
|
||||
export const getCollectionLatestFile = async (
|
||||
collections: collection[],
|
||||
data: file[]
|
||||
): Promise<collectionLatestFile[]> => {
|
||||
let collectionIdSet = new Set<number>();
|
||||
let collectionMap = new Map<number, collection>();
|
||||
collections.forEach((collection) => {
|
||||
collectionMap.set(Number(collection.id), collection);
|
||||
collectionIdSet.add(Number(collection.id))
|
||||
});
|
||||
return Promise.all(
|
||||
data
|
||||
.filter((item) => {
|
||||
if (collectionIdSet.size !== 0 && collectionIdSet.has(item.collectionID)) {
|
||||
collectionIdSet.delete(item.collectionID);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.map(async (item) => {
|
||||
const token = getData(LS_KEYS.USER).token;
|
||||
const url = await getPreview(token, item);
|
||||
return {
|
||||
file: item,
|
||||
collection: collectionMap.get(item.collectionID),
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@ import HTTPService from './HTTPService';
|
|||
import * as Comlink from 'comlink';
|
||||
import EXIF from "exif-js";
|
||||
import { fileAttribute } from './fileService';
|
||||
import { collectionLatestFile } from "./fileService"
|
||||
import { collection, collectionLatestFile } from "./collectionService"
|
||||
import { FILE_TYPE } from 'pages/gallery';
|
||||
const CryptoWorker: any =
|
||||
typeof window !== 'undefined' &&
|
||||
|
@ -26,19 +26,17 @@ interface uploadURL {
|
|||
objectKey: string
|
||||
}
|
||||
|
||||
interface formatedFile {
|
||||
interface FileinMemory {
|
||||
filedata: Uint8Array,
|
||||
metadata: Object,
|
||||
thumbnail: Uint8Array
|
||||
thumbnail: Uint8Array,
|
||||
filename: string
|
||||
}
|
||||
|
||||
|
||||
interface encryptedFile {
|
||||
filedata: fileAttribute;
|
||||
thumbnail: fileAttribute;
|
||||
metadata: fileAttribute;
|
||||
encryptedKey: string;
|
||||
keyDecryptionNonce: string;
|
||||
key: string;
|
||||
fileKey: keyEncryptionResult;
|
||||
}
|
||||
|
||||
interface objectKey {
|
||||
|
@ -54,36 +52,29 @@ interface uploadFile extends objectKeys {
|
|||
collectionID: string,
|
||||
encryptedKey: string;
|
||||
keyDecryptionNonce: string;
|
||||
metadata: {
|
||||
metadata?: {
|
||||
encryptedData: string | Uint8Array,
|
||||
decryptionHeader: string
|
||||
}
|
||||
}
|
||||
|
||||
class Queue<T> {
|
||||
_store: T[] = [];
|
||||
push(vals: T[]): void {
|
||||
vals.forEach((val) => this._store.push(val));
|
||||
}
|
||||
pop(): T {
|
||||
return this._store.shift();
|
||||
}
|
||||
isEmpty(): boolean {
|
||||
return this._store.length == 0;
|
||||
}
|
||||
interface UploadFileWithoutMetaData {
|
||||
tempUploadFile: uploadFile,
|
||||
encryptedFileKey: keyEncryptionResult,
|
||||
fileName: string
|
||||
}
|
||||
|
||||
export enum UPLOAD_STAGES {
|
||||
START = "Preparing to upload",
|
||||
ENCRYPTION = "Encryting your files",
|
||||
UPLOAD = "Uploading your Files",
|
||||
FINISH = "Files Uploaded Successfully !!!"
|
||||
START,
|
||||
ENCRYPTION,
|
||||
UPLOAD,
|
||||
FINISH
|
||||
}
|
||||
|
||||
class UploadService {
|
||||
|
||||
private uploadURLs = new Queue<uploadURL>();
|
||||
private uploadURLFetchInProgress: Promise<any> = null
|
||||
private uploadURLs: uploadURL[];
|
||||
private uploadURLFetchInProgress: Promise<any>;
|
||||
private perStepProgress: number
|
||||
private stepsCompleted: number
|
||||
private totalFilesCount: number
|
||||
|
@ -94,6 +85,8 @@ class UploadService {
|
|||
const worker = await new CryptoWorker();
|
||||
this.stepsCompleted = 0;
|
||||
this.metadataMap = new Map<string, object>();
|
||||
this.uploadURLs = [];
|
||||
this.uploadURLFetchInProgress = null;
|
||||
|
||||
let metadataFiles: File[] = [];
|
||||
let actualFiles: File[] = [];
|
||||
|
@ -105,37 +98,65 @@ class UploadService {
|
|||
});
|
||||
this.totalFilesCount = actualFiles.length;
|
||||
this.perStepProgress = 100 / (3 * actualFiles.length);
|
||||
let formatedFiles: formatedFile[] = await Promise.all(actualFiles.map(async (recievedFile: File) => {
|
||||
const file = await this.formatData(recievedFile);
|
||||
this.changeProgressBarProps(progressBarProps);
|
||||
return file;
|
||||
}));
|
||||
await Promise.all(metadataFiles.map(async (recievedFile: File) => {
|
||||
this.updateMetadata(recievedFile)
|
||||
this.changeProgressBarProps(progressBarProps);
|
||||
return;
|
||||
|
||||
}));
|
||||
console.log(formatedFiles);
|
||||
progressBarProps.setUploadStage(UPLOAD_STAGES.START);
|
||||
this.changeProgressBarProps(progressBarProps);
|
||||
|
||||
const uploadFilesWithoutMetaData: UploadFileWithoutMetaData[] = [];
|
||||
|
||||
while (actualFiles.length > 0) {
|
||||
var promises = [];
|
||||
for (var i = 0; i < 5 && actualFiles.length > 0; i++)
|
||||
promises.push(this.uploadHelper(progressBarProps, actualFiles.pop(), collectionLatestFile.collection, token));
|
||||
uploadFilesWithoutMetaData.push(...await Promise.all(promises));
|
||||
}
|
||||
|
||||
for await (const rawFile of metadataFiles) {
|
||||
await this.updateMetadata(rawFile)
|
||||
};
|
||||
|
||||
progressBarProps.setUploadStage(UPLOAD_STAGES.ENCRYPTION);
|
||||
const encryptedFiles: encryptedFile[] = await Promise.all(formatedFiles.map(async (file: formatedFile) => {
|
||||
const encryptedFile = await this.encryptFiles(worker, file, collectionLatestFile.collection.key);
|
||||
const completeUploadFiles: uploadFile[] = await Promise.all(uploadFilesWithoutMetaData.map(async (file: UploadFileWithoutMetaData) => {
|
||||
const { file: encryptedMetaData } = await this.encryptMetadata(worker, file.fileName, file.encryptedFileKey);
|
||||
const completeUploadFile = {
|
||||
...file.tempUploadFile,
|
||||
metadata: {
|
||||
encryptedData: encryptedMetaData.encryptedData,
|
||||
decryptionHeader: encryptedMetaData.decryptionHeader
|
||||
}
|
||||
}
|
||||
this.changeProgressBarProps(progressBarProps);
|
||||
return encryptedFile;
|
||||
return completeUploadFile;
|
||||
}));
|
||||
|
||||
progressBarProps.setUploadStage(UPLOAD_STAGES.UPLOAD);
|
||||
await Promise.all(encryptedFiles.map(async (encryptedFile: encryptedFile) => {
|
||||
await Promise.all(completeUploadFiles.map(async (uploadFile: uploadFile) => {
|
||||
|
||||
const objectKeys = await this.uploadtoBucket(encryptedFile, token, 2 * this.totalFilesCount);
|
||||
await this.uploadFile(collectionLatestFile, encryptedFile, objectKeys, token);
|
||||
await this.uploadFile(uploadFile, token);
|
||||
this.changeProgressBarProps(progressBarProps);
|
||||
|
||||
}));
|
||||
|
||||
progressBarProps.setUploadStage(UPLOAD_STAGES.FINISH);
|
||||
progressBarProps.setPercentComplete(100);
|
||||
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
private async uploadHelper(progressBarProps, rawFile, collection, token) {
|
||||
try {
|
||||
const worker = await new CryptoWorker();
|
||||
let file: FileinMemory = await this.readFile(rawFile);
|
||||
let encryptedFile: encryptedFile = await this.encryptFile(worker, file, collection.key);
|
||||
let objectKeys = await this.uploadtoBucket(encryptedFile, token, 2 * this.totalFilesCount);
|
||||
let uploadFileWithoutMetaData: uploadFile = this.getuploadFile(collection, encryptedFile.fileKey, objectKeys);
|
||||
this.changeProgressBarProps(progressBarProps);
|
||||
|
||||
return {
|
||||
tempUploadFile: uploadFileWithoutMetaData,
|
||||
encryptedFileKey: encryptedFile.fileKey,
|
||||
fileName: file.filename
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e);
|
||||
|
@ -145,212 +166,287 @@ class UploadService {
|
|||
private changeProgressBarProps({ setPercentComplete, setFileCounter }) {
|
||||
this.stepsCompleted++;
|
||||
const fileCompleted = this.stepsCompleted % this.totalFilesCount;
|
||||
setFileCounter({ current: fileCompleted + 1, total: this.totalFilesCount });
|
||||
setFileCounter({ current: fileCompleted, total: this.totalFilesCount });
|
||||
setPercentComplete(this.perStepProgress * this.stepsCompleted);
|
||||
}
|
||||
|
||||
private async formatData(recievedFile: File) {
|
||||
const filedata: Uint8Array = await this.getUint8ArrayView(recievedFile);
|
||||
let fileType;
|
||||
switch (recievedFile.type.split('/')[0]) {
|
||||
case "image":
|
||||
fileType = FILE_TYPE.IMAGE;
|
||||
break;
|
||||
case "video":
|
||||
fileType = FILE_TYPE.VIDEO;
|
||||
default:
|
||||
fileType = FILE_TYPE.OTHERS;
|
||||
}
|
||||
private async readFile(recievedFile: File) {
|
||||
try {
|
||||
const filedata: Uint8Array = await this.getUint8ArrayView(recievedFile);
|
||||
let fileType;
|
||||
switch (recievedFile.type.split('/')[0]) {
|
||||
case "image":
|
||||
fileType = FILE_TYPE.IMAGE;
|
||||
break;
|
||||
case "video":
|
||||
fileType = FILE_TYPE.VIDEO;
|
||||
break;
|
||||
default:
|
||||
fileType = FILE_TYPE.OTHERS;
|
||||
}
|
||||
|
||||
const { location, creationTime } = await this.getExifData(recievedFile);
|
||||
this.metadataMap.set(recievedFile.name, {
|
||||
title: recievedFile.name,
|
||||
creationTime: creationTime || (recievedFile.lastModified) * 1000,
|
||||
modificationTime: (recievedFile.lastModified) * 1000,
|
||||
latitude: location?.lat,
|
||||
longitude: location?.lon,
|
||||
fileType,
|
||||
});
|
||||
return {
|
||||
filedata,
|
||||
metadata: this.metadataMap.get(recievedFile.name),
|
||||
thumbnail: await this.generateThumbnail(recievedFile)
|
||||
const { location, creationTime } = await this.getExifData(recievedFile);
|
||||
this.metadataMap.set(recievedFile.name, {
|
||||
title: recievedFile.name,
|
||||
creationTime: creationTime || (recievedFile.lastModified) * 1000,
|
||||
modificationTime: (recievedFile.lastModified) * 1000,
|
||||
latitude: location?.latitude,
|
||||
longitude: location?.latitude,
|
||||
fileType,
|
||||
});
|
||||
return {
|
||||
filedata,
|
||||
filename: recievedFile.name,
|
||||
thumbnail: await this.generateThumbnail(recievedFile)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("error reading files " + e);
|
||||
}
|
||||
}
|
||||
private async encryptFiles(worker, file: formatedFile, encryptionKey: string): Promise<encryptedFile> {
|
||||
private async encryptFile(worker, file: FileinMemory, encryptionKey: string): Promise<encryptedFile> {
|
||||
try {
|
||||
|
||||
const { key: fileKey, file: encryptedFiledata }: encryptionResult = await worker.encryptFile(file.filedata);
|
||||
|
||||
const { file: encryptedThumbnail }: encryptionResult = await worker.encryptThumbnail(file.thumbnail, fileKey);
|
||||
|
||||
const encryptedKey: keyEncryptionResult = await worker.encryptToB64(fileKey, encryptionKey);
|
||||
|
||||
|
||||
const { key: fileKey, file: filedata }: encryptionResult = await worker.encryptFile(file.filedata);
|
||||
const result: encryptedFile = {
|
||||
filedata: encryptedFiledata,
|
||||
thumbnail: encryptedThumbnail,
|
||||
fileKey: encryptedKey
|
||||
};
|
||||
return result;
|
||||
}
|
||||
catch (e) {
|
||||
console.log("Error encrypting files " + e);
|
||||
}
|
||||
}
|
||||
|
||||
const { file: encryptedThumbnail }: encryptionResult = await worker.encryptThumbnail(file.thumbnail, fileKey);
|
||||
|
||||
const { file: encryptedMetadata }: encryptionResult = await worker.encryptMetadata(file.metadata, fileKey)
|
||||
|
||||
const { encryptedData: encryptedKey, nonce: keyDecryptionNonce }: keyEncryptionResult = await worker.encryptToB64(fileKey, encryptionKey);
|
||||
|
||||
|
||||
const result: encryptedFile = {
|
||||
key: fileKey,
|
||||
filedata: filedata,
|
||||
thumbnail: encryptedThumbnail,
|
||||
metadata: encryptedMetadata,
|
||||
encryptedKey,
|
||||
keyDecryptionNonce,
|
||||
};
|
||||
return result;
|
||||
private async encryptMetadata(worker: any, fileName: string, encryptedFileKey: keyEncryptionResult) {
|
||||
const metaData = this.metadataMap.get(fileName);
|
||||
const fileKey = await worker.decryptB64(encryptedFileKey.encryptedData, encryptedFileKey.nonce, encryptedFileKey.key);
|
||||
const encryptedMetaData = await worker.encryptMetadata(metaData, fileKey);
|
||||
return encryptedMetaData;
|
||||
}
|
||||
|
||||
private async uploadtoBucket(file: encryptedFile, token, count: number): Promise<objectKeys> {
|
||||
const fileUploadURL = await this.getUploadURL(token, count);
|
||||
const fileObjectKey = await this.putFile(fileUploadURL, file.filedata.encryptedData)
|
||||
try {
|
||||
const fileUploadURL = await this.getUploadURL(token, count);
|
||||
const fileObjectKey = await this.putFile(fileUploadURL, file.filedata.encryptedData)
|
||||
|
||||
const thumbnailUploadURL = await this.getUploadURL(token, count);
|
||||
const thumbnailObjectKey = await this.putFile(thumbnailUploadURL, file.thumbnail.encryptedData)
|
||||
const thumbnailUploadURL = await this.getUploadURL(token, count);
|
||||
const thumbnailObjectKey = await this.putFile(thumbnailUploadURL, file.thumbnail.encryptedData)
|
||||
|
||||
return {
|
||||
file: { objectKey: fileObjectKey, decryptionHeader: file.filedata.decryptionHeader },
|
||||
thumbnail: { objectKey: thumbnailObjectKey, decryptionHeader: file.thumbnail.decryptionHeader }
|
||||
};
|
||||
return {
|
||||
file: { objectKey: fileObjectKey, decryptionHeader: file.filedata.decryptionHeader },
|
||||
thumbnail: { objectKey: thumbnailObjectKey, decryptionHeader: file.thumbnail.decryptionHeader }
|
||||
};
|
||||
} catch (e) {
|
||||
console.log("error uploading to bucket " + e);
|
||||
}
|
||||
}
|
||||
|
||||
private async uploadFile(collectionLatestFile: collectionLatestFile, encryptedFile: encryptedFile, objectKeys: objectKeys, token) {
|
||||
private getuploadFile(collection: collection, encryptedKey: keyEncryptionResult, objectKeys: objectKeys): uploadFile {
|
||||
const uploadFile: uploadFile = {
|
||||
collectionID: collectionLatestFile.collection.id,
|
||||
encryptedKey: encryptedFile.encryptedKey,
|
||||
keyDecryptionNonce: encryptedFile.keyDecryptionNonce,
|
||||
metadata: {
|
||||
encryptedData: encryptedFile.metadata.encryptedData,
|
||||
decryptionHeader: encryptedFile.metadata.decryptionHeader
|
||||
},
|
||||
collectionID: collection.id,
|
||||
encryptedKey: encryptedKey.encryptedData,
|
||||
keyDecryptionNonce: encryptedKey.nonce,
|
||||
...objectKeys
|
||||
}
|
||||
return uploadFile;
|
||||
}
|
||||
|
||||
private async uploadFile(uploadFile: uploadFile, token) {
|
||||
try {
|
||||
const response = await HTTPService.post(`${ENDPOINT}/files`, uploadFile, null, { 'X-Auth-Token': token });
|
||||
|
||||
const response = await HTTPService.post(`${ENDPOINT}/files`, uploadFile, { token });
|
||||
|
||||
return response.data;
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
console.log("upload Files Failed " + e);
|
||||
}
|
||||
}
|
||||
|
||||
private async updateMetadata(recievedFile: File) {
|
||||
|
||||
const metadataJSON: object = await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
var result = typeof reader.result !== "string" ? new TextDecoder().decode(reader.result) : reader.result
|
||||
resolve(JSON.parse(result));
|
||||
}
|
||||
reader.readAsText(recievedFile)
|
||||
});
|
||||
if (!this.metadataMap.has(metadataJSON['title']))
|
||||
return;
|
||||
|
||||
const metaDataObject = this.metadataMap.get(metadataJSON['title']);
|
||||
metaDataObject['creationTime'] = metadataJSON['photoTakenTime']['timestamp'] * 1000000;
|
||||
metaDataObject['modificationTime'] = metadataJSON['modificationTime']['timestamp'] * 1000000;
|
||||
if (!metaDataObject['latitude'] && metaDataObject['latitude'] != 0 && metaDataObject['longitude'] != 0) {
|
||||
metaDataObject['latitude'] = metadataJSON['geoData']['latitude'];
|
||||
metaDataObject['longitude'] = metadataJSON['geoData']['longitude'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async generateThumbnail(file: File): Promise<Uint8Array> {
|
||||
let canvas = document.createElement("canvas");
|
||||
let canvas_CTX = canvas.getContext("2d");
|
||||
let type = file.type.split('/')[0];
|
||||
if (type === "image") {
|
||||
let image = new Image();
|
||||
image.setAttribute("src", URL.createObjectURL(file));
|
||||
await new Promise((resolve, reject) => {
|
||||
image.onload = () => {
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
canvas_CTX.drawImage(image, 0, 0, image.width, image.height);
|
||||
image = undefined;
|
||||
resolve(null);
|
||||
try {
|
||||
const metadataJSON: object = await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
var result = typeof reader.result !== "string" ? new TextDecoder().decode(reader.result) : reader.result
|
||||
resolve(JSON.parse(result));
|
||||
}
|
||||
reader.readAsText(recievedFile)
|
||||
});
|
||||
if (!this.metadataMap.has(metadataJSON['title']))
|
||||
return;
|
||||
|
||||
const metaDataObject = this.metadataMap.get(metadataJSON['title']);
|
||||
metaDataObject['creationTime'] = metadataJSON['photoTakenTime']['timestamp'] * 1000000;
|
||||
metaDataObject['modificationTime'] = metadataJSON['modificationTime']['timestamp'] * 1000000;
|
||||
|
||||
if (metaDataObject['latitude'] == null || (metaDataObject['latitude'] == 0.0 && metaDataObject['longitude'] == 0.0)) {
|
||||
var locationData = null;
|
||||
if (metadataJSON['geoData']['latitude'] != 0.0 || metadataJSON['geoData']['longitude'] != 0.0) {
|
||||
locationData = metadataJSON['geoData'];
|
||||
}
|
||||
else if (metadataJSON['geoDataExif']['latitude'] != 0.0 || metadataJSON['geoDataExif']['longitude'] != 0.0) {
|
||||
locationData = metadataJSON['geoDataExif'];
|
||||
}
|
||||
if (locationData != null) {
|
||||
metaDataObject['latitude'] = locationData['latitide'];
|
||||
metaDataObject['longitude'] = locationData['longitude'];
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("error reading metaData Files " + e);
|
||||
|
||||
}
|
||||
else {
|
||||
let video = document.createElement('video');
|
||||
video.setAttribute("src", URL.createObjectURL(file));
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
video.addEventListener('loadedmetadata', function () {
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
canvas_CTX.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
|
||||
video = undefined;
|
||||
resolve(null);
|
||||
}
|
||||
private async generateThumbnail(file: File): Promise<Uint8Array> {
|
||||
try {
|
||||
let canvas = document.createElement("canvas");
|
||||
let canvas_CTX = canvas.getContext("2d");
|
||||
let imageURL = null;
|
||||
if (file.type.match("image")) {
|
||||
let image = new Image();
|
||||
imageURL = URL.createObjectURL(file);
|
||||
image.setAttribute("src", imageURL);
|
||||
await new Promise((resolve) => {
|
||||
image.onload = () => {
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
canvas_CTX.drawImage(image, 0, 0, image.width, image.height);
|
||||
image = undefined;
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
else {
|
||||
await new Promise(async (resolve) => {
|
||||
let video = document.createElement('video');
|
||||
imageURL = URL.createObjectURL(file);
|
||||
var timeupdate = function () {
|
||||
if (snapImage()) {
|
||||
video.removeEventListener('timeupdate', timeupdate);
|
||||
video.pause();
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
video.addEventListener('loadeddata', function () {
|
||||
if (snapImage()) {
|
||||
video.removeEventListener('timeupdate', timeupdate);
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
var snapImage = function () {
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
canvas_CTX.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
var image = canvas.toDataURL();
|
||||
var success = image.length > 100000;
|
||||
return success;
|
||||
};
|
||||
video.addEventListener('timeupdate', timeupdate);
|
||||
video.preload = 'metadata';
|
||||
video.src = imageURL;
|
||||
// Load video in Safari / IE11
|
||||
video.muted = true;
|
||||
video.playsInline = true;
|
||||
video.play();
|
||||
});
|
||||
}
|
||||
URL.revokeObjectURL(imageURL);
|
||||
var thumbnailBlob = await new Promise(resolve => {
|
||||
canvas.toBlob(function (blob) {
|
||||
resolve(blob);
|
||||
}), 'image/jpeg', 0.4
|
||||
});
|
||||
console.log(URL.createObjectURL(thumbnailBlob));
|
||||
const thumbnail = this.getUint8ArrayView(thumbnailBlob);
|
||||
return thumbnail;
|
||||
} catch (e) {
|
||||
console.log("Error generatin thumbnail " + e);
|
||||
}
|
||||
const thumbnail: Uint8Array = await new Promise((resolve, reject) => {
|
||||
canvas.toBlob(async (blob) => {
|
||||
resolve(await this.getUint8ArrayView(blob));
|
||||
})
|
||||
});
|
||||
return thumbnail;
|
||||
}
|
||||
|
||||
private async getUint8ArrayView(file): Promise<Uint8Array> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
try {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onabort = () => reject('file reading was aborted')
|
||||
reader.onerror = () => reject('file reading has failed')
|
||||
reader.onload = () => {
|
||||
// Do whatever you want with the file contents
|
||||
const result = typeof reader.result === "string" ? new TextEncoder().encode(reader.result) : new Uint8Array(reader.result);
|
||||
resolve(result);
|
||||
}
|
||||
reader.readAsArrayBuffer(file)
|
||||
});
|
||||
reader.onabort = () => reject('file reading was aborted')
|
||||
reader.onerror = () => reject('file reading has failed')
|
||||
reader.onload = () => {
|
||||
// Do whatever you want with the file contents
|
||||
const result = typeof reader.result === "string" ? new TextEncoder().encode(reader.result) : new Uint8Array(reader.result);
|
||||
resolve(result);
|
||||
}
|
||||
reader.readAsArrayBuffer(file)
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("error readinf file to bytearray " + e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private async getUploadURL(token: string, count: number) {
|
||||
if (this.uploadURLs.isEmpty()) {
|
||||
if (this.uploadURLs.length == 0) {
|
||||
await this.fetchUploadURLs(token, count);
|
||||
}
|
||||
return this.uploadURLs.pop();
|
||||
}
|
||||
|
||||
private async fetchUploadURLs(token: string, count: number): Promise<void> {
|
||||
if (!this.uploadURLFetchInProgress) {
|
||||
this.uploadURLFetchInProgress = HTTPService.get(`${ENDPOINT}/files/upload-urls`,
|
||||
{
|
||||
token: token,
|
||||
count: Math.min(50, count).toString() //m4gic number
|
||||
})
|
||||
const response = await this.uploadURLFetchInProgress;
|
||||
try {
|
||||
if (!this.uploadURLFetchInProgress) {
|
||||
this.uploadURLFetchInProgress = HTTPService.get(`${ENDPOINT}/files/upload-urls`,
|
||||
{
|
||||
count: Math.min(50, count).toString() //m4gic number
|
||||
}, { 'X-Auth-Token': token })
|
||||
const response = await this.uploadURLFetchInProgress;
|
||||
|
||||
this.uploadURLFetchInProgress = null;
|
||||
this.uploadURLs.push(response.data["urls"]);
|
||||
this.uploadURLFetchInProgress = null;
|
||||
this.uploadURLs.push(...response.data["urls"]);
|
||||
}
|
||||
return this.uploadURLFetchInProgress;
|
||||
} catch (e) {
|
||||
console.log("fetch upload-url failed " + e);
|
||||
throw e;
|
||||
}
|
||||
return this.uploadURLFetchInProgress;
|
||||
}
|
||||
|
||||
private async putFile(fileUploadURL: uploadURL, file: Uint8Array | string): Promise<string> {
|
||||
const fileSize = file.length.toString();
|
||||
await HTTPService.put(fileUploadURL.url, file, null, { contentLengthHeader: fileSize })
|
||||
return fileUploadURL.objectKey;
|
||||
try {
|
||||
const fileSize = file.length.toString();
|
||||
await HTTPService.put(fileUploadURL.url, file, null, { contentLengthHeader: fileSize })
|
||||
return fileUploadURL.objectKey;
|
||||
} catch (e) {
|
||||
console.log('putFile to dataStore failed ' + e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private async getExifData(recievedFile) {
|
||||
const exifData: any = await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
resolve(EXIF.readFromBinaryFile(reader.result));
|
||||
}
|
||||
reader.readAsArrayBuffer(recievedFile)
|
||||
});
|
||||
if (!exifData)
|
||||
return { location: null, creationTime: null };
|
||||
return {
|
||||
location: this.getLocation(exifData),
|
||||
creationTime: this.getUNIXTime(exifData)
|
||||
};
|
||||
try {
|
||||
const exifData: any = await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
resolve(EXIF.readFromBinaryFile(reader.result));
|
||||
}
|
||||
reader.readAsArrayBuffer(recievedFile)
|
||||
});
|
||||
if (!exifData)
|
||||
return { location: null, creationTime: null };
|
||||
return {
|
||||
location: this.getLocation(exifData),
|
||||
creationTime: this.getUNIXTime(exifData)
|
||||
};
|
||||
} catch (e) {
|
||||
console.log("error reading exif data");
|
||||
}
|
||||
}
|
||||
private getUNIXTime(exifData: any) {
|
||||
if (!exifData.DateTimeOriginal)
|
||||
|
@ -380,7 +476,7 @@ class UploadService {
|
|||
|
||||
var lonFinal = this.convertDMSToDD(lonDegree, lonMinute, lonSecond, lonDirection);
|
||||
|
||||
return { lat: latFinal, lon: lonFinal };
|
||||
return { latitude: latFinal * 1.0, longitude: lonFinal * 1.0 };
|
||||
}
|
||||
|
||||
private convertDMSToDD(degrees, minutes, seconds, direction) {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
export const getEndpoint = () => {
|
||||
return process.env.NEXT_PUBLIC_ENTE_ENDPOINT || "https://api.ente.io";
|
||||
const endPoint = process.env.NEXT_PUBLIC_ENTE_ENDPOINT ?? "https://api.ente.io";
|
||||
console.log(endPoint);
|
||||
return endPoint;
|
||||
}
|
||||
|
|
|
@ -14,3 +14,7 @@ export const getActualKey = async () => {
|
|||
const key: string = await cryptoWorker.decryptB64(encryptedKey, session.sessionNonce, session.sessionKey);
|
||||
return key;
|
||||
}
|
||||
|
||||
export const getToken = () => {
|
||||
return getData(LS_KEYS.USER)?.token;
|
||||
}
|
||||
|
|
|
@ -107,6 +107,15 @@ export async function decryptB64(data: string, nonce: string, key: string) {
|
|||
return await toB64(decrypted);
|
||||
}
|
||||
|
||||
export async function decryptString(data: string, nonce: string, key: string) {
|
||||
await sodium.ready;
|
||||
const decrypted = await decrypt(await fromB64(data),
|
||||
await fromB64(nonce),
|
||||
await fromB64(key));
|
||||
|
||||
return sodium.to_string(decrypted);
|
||||
}
|
||||
|
||||
export async function encrypt(data: Uint8Array, key?: Uint8Array) {
|
||||
await sodium.ready;
|
||||
const uintkey: Uint8Array = key ? key : sodium.crypto_secretbox_keygen();
|
||||
|
@ -173,7 +182,16 @@ export async function boxSealOpen(input: string, publicKey: string, secretKey: s
|
|||
|
||||
export async function fromB64(input: string) {
|
||||
await sodium.ready;
|
||||
return sodium.from_base64(input, sodium.base64_variants.ORIGINAL);
|
||||
let result;
|
||||
try {
|
||||
result = sodium.from_base64(input, sodium.base64_variants.ORIGINAL);
|
||||
} catch (e) {
|
||||
result = await fromB64(await toB64(await fromString(input)));
|
||||
|
||||
}
|
||||
finally {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export async function toB64(input: Uint8Array) {
|
||||
|
|
|
@ -41,7 +41,17 @@ const englishConstants = {
|
|||
PASSPHRASE_CONFIRM: 'Please repeat it once more',
|
||||
PASSPHRASE_MATCH_ERROR: `Passphrase didn't match`,
|
||||
CONSOLE_WARNING_STOP: 'STOP!',
|
||||
CONSOLE_WARNING_DESC: `This is a browser feature intended for developers. If someone told you to copy-paste something here to enable a feature or "hack" someone's account, it is a scam and will give them access to your account.`
|
||||
CONSOLE_WARNING_DESC: `This is a browser feature intended for developers. If someone told you to copy-paste something here to enable a feature or "hack" someone's account, it is a scam and will give them access to your account.`,
|
||||
SELECT_COLLECTION: `Select/Click on Collection to upload`,
|
||||
CLOSE: 'Close',
|
||||
NOTHING_HERE: `nothing to see here! 👀`,
|
||||
UPLOAD: {
|
||||
0: "Preparing to upload",
|
||||
1: "Encryting your files",
|
||||
2: "Uploading your Files",
|
||||
3: "Files Uploaded Successfully !!!"
|
||||
},
|
||||
OF: 'of'
|
||||
};
|
||||
|
||||
export default englishConstants;
|
||||
|
|
|
@ -74,6 +74,10 @@ export class Crypto {
|
|||
return libsodium.decryptB64(data, nonce, key)
|
||||
}
|
||||
|
||||
async decryptString(data, nonce, key) {
|
||||
return libsodium.decryptString(data, nonce, key)
|
||||
}
|
||||
|
||||
async encryptToB64(data, key) {
|
||||
return libsodium.encryptToB64(data, key);
|
||||
}
|
||||
|
|
|
@ -1,32 +1,33 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext",
|
||||
"webworker"
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext",
|
||||
"webworker"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"baseUrl": "./src",
|
||||
"downlevelIteration": true
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"src/pages/index.tsx"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"baseUrl": "./src",
|
||||
"downlevelIteration": true
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx", "src/pages/index.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
Loading…
Add table
Reference in a new issue