Merge pull request #6 from ente-io/collection-ops

Collection Create and Add to favorite
This commit is contained in:
Pushkar Anand 2021-02-07 20:19:53 +05:30 committed by GitHub
commit 5ea0b6caae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 1576 additions and 861 deletions

BIN
public/plus-sign.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

File diff suppressed because one or more lines are too long

View file

@ -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>
);
};

View 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;

View file

@ -0,0 +1,19 @@
export default [
'beforeChange',
'afterChange',
'imageLoadComplete',
'resize',
'gettingData',
'mouseUsed',
'initialZoomIn',
'initialZoomInEnd',
'initialZoomOut',
'initialZoomOutEnd',
'parseVerticalMargin',
'close',
'unbindEvents',
'destroy',
'updateScrollOffset',
'preventDragEvent',
'shareLinkClick'
];

View file

@ -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>
);

View 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}
/>
</>
)
}

View file

@ -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>
/>
);
};

View file

@ -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>
);
}

View file

@ -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>)}

View 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>
);
}

View 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;

View file

@ -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>;

View file

@ -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 }) {

View file

@ -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} />
</>
)}

View file

@ -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>
)}
</>
);
}

View 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);
}
}

View file

@ -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),
};
})
);
};

View file

@ -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) {

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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) {

View file

@ -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;

View file

@ -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);
}

View file

@ -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"
]
}