commit
9f6388b8ef
31 changed files with 1997 additions and 348 deletions
|
@ -12,6 +12,7 @@
|
|||
"axios": "^0.20.0",
|
||||
"bootstrap": "^4.5.2",
|
||||
"comlink": "^4.3.0",
|
||||
"exif-js": "^2.3.0",
|
||||
"formik": "^2.1.5",
|
||||
"http-proxy-middleware": "^1.0.5",
|
||||
"libsodium-wrappers": "^0.7.8",
|
||||
|
@ -21,6 +22,7 @@
|
|||
"react": "16.13.1",
|
||||
"react-bootstrap": "^1.3.0",
|
||||
"react-dom": "16.13.1",
|
||||
"react-dropzone": "^11.2.4",
|
||||
"react-photoswipe": "^1.3.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
"react-window": "^1.8.6",
|
||||
|
@ -42,7 +44,7 @@
|
|||
"@types/yup": "^0.29.7",
|
||||
"babel-plugin-styled-components": "^1.11.1",
|
||||
"next-on-netlify": "^2.4.0",
|
||||
"typescript": "^4.0.2",
|
||||
"typescript": "^4.1.3",
|
||||
"worker-plugin": "^5.0.0"
|
||||
},
|
||||
"standard": {
|
||||
|
|
BIN
public/fav-button.png
Normal file
BIN
public/fav-button.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
BIN
public/plus-sign.png
Normal file
BIN
public/plus-sign.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3 KiB |
23
src/components/FavButton.tsx
Normal file
23
src/components/FavButton.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
const HeartUI = styled.button<{
|
||||
isClick: boolean,
|
||||
size: number,
|
||||
}>`
|
||||
width: ${props => props.size}px;
|
||||
height: ${props => props.size}px;
|
||||
float:right;
|
||||
background: url("/fav-button.png") no-repeat;
|
||||
cursor: pointer;
|
||||
background-size: cover;
|
||||
border: none;
|
||||
${({ isClick, size }) => isClick && `background-position: -${28 * size}px;transition: background 1s steps(28);`}
|
||||
`;
|
||||
|
||||
|
||||
export default function FavButton({ isClick, onClick, size }) {
|
||||
return (
|
||||
<HeartUI isClick={isClick} onClick={onClick} size={size}/>
|
||||
);
|
||||
}
|
41
src/components/FullScreenDropZone.tsx
Normal file
41
src/components/FullScreenDropZone.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import React, { useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const DropDiv = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
type Props = React.PropsWithChildren<{
|
||||
showModal: () => void;
|
||||
closeModal: () => void;
|
||||
}>;
|
||||
|
||||
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'
|
||||
];
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import styled, {createGlobalStyle } from 'styled-components';
|
||||
import styled, { createGlobalStyle } from 'styled-components';
|
||||
import Navbar from 'components/Navbar';
|
||||
import constants from 'utils/strings/constants';
|
||||
import Button from 'react-bootstrap/Button';
|
||||
|
@ -13,6 +13,8 @@ import Head from 'next/head';
|
|||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'react-photoswipe/lib/photoswipe.css';
|
||||
import localForage from 'localforage';
|
||||
import UploadButton from 'pages/gallery/components/UploadButton';
|
||||
import FullScreenDropZone from 'components/FullScreenDropZone';
|
||||
|
||||
localForage.config({
|
||||
driver: localForage.INDEXEDDB,
|
||||
|
@ -78,26 +80,59 @@ const GlobalStyles = createGlobalStyle`
|
|||
.pswp__img {
|
||||
object-fit: contain;
|
||||
}
|
||||
.modal-90w{
|
||||
width:90vw;
|
||||
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:#aaa;
|
||||
}
|
||||
`;
|
||||
|
||||
const Image = styled.img`
|
||||
max-height: 28px;
|
||||
margin-right: 5px;
|
||||
max-height: 28px;
|
||||
margin-right: 5px;
|
||||
`;
|
||||
|
||||
const FlexContainer = styled.div`
|
||||
flex: 1;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
export default function App({ Component, pageProps }) {
|
||||
const router = useRouter();
|
||||
const [user, setUser] = useState();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [uploadButtonView, setUploadButtonView] = useState(false);
|
||||
const [uploadModalView, setUploadModalView] = useState(false);
|
||||
|
||||
const closeUploadModal = () => setUploadModalView(false);
|
||||
const showUploadModal = () => setUploadModalView(true);
|
||||
|
||||
useEffect(() => {
|
||||
const user = getData(LS_KEYS.USER);
|
||||
setUser(user);
|
||||
console.log(`%c${constants.CONSOLE_WARNING_STOP}`, 'color: red; font-size: 52px;');
|
||||
console.log(
|
||||
`%c${constants.CONSOLE_WARNING_STOP}`,
|
||||
'color: red; font-size: 52px;'
|
||||
);
|
||||
console.log(`%c${constants.CONSOLE_WARNING_DESC}`, 'font-size: 20px;');
|
||||
|
||||
router.events.on('routeChangeStart', (url: string) => {
|
||||
|
@ -116,34 +151,47 @@ export default function App({ Component, pageProps }) {
|
|||
const logout = async () => {
|
||||
clearKeys();
|
||||
clearData();
|
||||
setUploadButtonView(false);
|
||||
localForage.clear();
|
||||
const cache = await caches.delete('thumbs');
|
||||
router.push("/");
|
||||
}
|
||||
router.push('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FullScreenDropZone
|
||||
closeModal={closeUploadModal}
|
||||
showModal={showUploadModal}
|
||||
>
|
||||
<Head>
|
||||
<title>ente.io | Privacy friendly alternative to Google Photos</title>
|
||||
</Head>
|
||||
<GlobalStyles />
|
||||
<Navbar>
|
||||
<FlexContainer>
|
||||
<Image alt='logo' src="/icon.png" />
|
||||
<Image alt='logo' src='/icon.png' />
|
||||
{constants.COMPANY_NAME}
|
||||
</FlexContainer>
|
||||
{user && <Button variant='link' onClick={logout}>
|
||||
<PowerSettings />
|
||||
</Button>}
|
||||
{uploadButtonView && <UploadButton showModal={showUploadModal} />}
|
||||
{user &&
|
||||
<Button variant='link' onClick={logout}>
|
||||
<PowerSettings />
|
||||
</Button>
|
||||
}
|
||||
</Navbar>
|
||||
{loading
|
||||
? <Container>
|
||||
<Spinner animation="border" role="status" variant="primary">
|
||||
<span className="sr-only">Loading...</span>
|
||||
{loading ? (
|
||||
<Container>
|
||||
<Spinner animation='border' role='status' variant='primary'>
|
||||
<span className='sr-only'>Loading...</span>
|
||||
</Spinner>
|
||||
</Container>
|
||||
: <Component />
|
||||
}
|
||||
</>
|
||||
) : (
|
||||
<Component
|
||||
uploadModalView={uploadModalView}
|
||||
showUploadModal={showUploadModal}
|
||||
closeUploadModal={closeUploadModal}
|
||||
setUploadButtonView={setUploadButtonView}
|
||||
/>
|
||||
)}
|
||||
</FullScreenDropZone>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,18 +52,14 @@ export default function Credentials() {
|
|||
try {
|
||||
const cryptoWorker = await new CryptoWorker();
|
||||
const { passphrase } = values;
|
||||
const kek = await cryptoWorker.deriveKey(await cryptoWorker.fromString(passphrase),
|
||||
await cryptoWorker.fromB64(keyAttributes.kekSalt));
|
||||
const kek: string = await cryptoWorker.deriveKey(passphrase, keyAttributes.kekSalt);
|
||||
|
||||
if (await cryptoWorker.verifyHash(keyAttributes.kekHash, kek)) {
|
||||
const key = await cryptoWorker.decrypt(
|
||||
await cryptoWorker.fromB64(keyAttributes.encryptedKey),
|
||||
await cryptoWorker.fromB64(keyAttributes.keyDecryptionNonce),
|
||||
kek);
|
||||
const sessionKeyAttributes = await cryptoWorker.encrypt(key);
|
||||
const sessionKey = await cryptoWorker.toB64(sessionKeyAttributes.key);
|
||||
const sessionNonce = await cryptoWorker.toB64(sessionKeyAttributes.nonce);
|
||||
const encryptionKey = await cryptoWorker.toB64(sessionKeyAttributes.encryptedData);
|
||||
const key: string = await cryptoWorker.decryptB64(keyAttributes.encryptedKey, keyAttributes.keyDecryptionNonce, kek);
|
||||
const sessionKeyAttributes = await cryptoWorker.encryptToB64(key);
|
||||
const sessionKey = sessionKeyAttributes.key;
|
||||
const sessionNonce = sessionKeyAttributes.nonce;
|
||||
const encryptionKey = sessionKeyAttributes.encryptedData;
|
||||
setKey(SESSION_KEYS.ENCRYPTION_KEY, { encryptionKey });
|
||||
setData(LS_KEYS.SESSION, { sessionKey, sessionNonce });
|
||||
router.push('/gallery');
|
||||
|
|
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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
38
src/pages/gallery/components/CollectionDropZone.tsx
Normal file
38
src/pages/gallery/components/CollectionDropZone.tsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
import React from 'react';
|
||||
import UploadService from 'services/uploadService';
|
||||
import { getToken } from 'utils/common/key';
|
||||
import DropzoneWrapper from './DropzoneWrapper';
|
||||
|
||||
|
||||
function CollectionDropZone({
|
||||
children,
|
||||
closeModal,
|
||||
showModal,
|
||||
refetchData,
|
||||
collectionLatestFile,
|
||||
setProgressView,
|
||||
progressBarProps
|
||||
|
||||
}) {
|
||||
|
||||
const upload = async (acceptedFiles) => {
|
||||
const token = getToken();
|
||||
closeModal();
|
||||
progressBarProps.setPercentComplete(0);
|
||||
setProgressView(true);
|
||||
|
||||
await UploadService.uploadFiles(acceptedFiles, collectionLatestFile, token, progressBarProps);
|
||||
refetchData();
|
||||
setProgressView(false);
|
||||
}
|
||||
return (
|
||||
<DropzoneWrapper
|
||||
children={children}
|
||||
onDropAccepted={upload}
|
||||
onDragOver={showModal}
|
||||
onDropRejected={closeModal}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionDropZone;
|
56
src/pages/gallery/components/CollectionSelector.tsx
Normal file
56
src/pages/gallery/components/CollectionSelector.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Card, Modal } from 'react-bootstrap';
|
||||
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;
|
||||
|
||||
|
||||
const CollectionIcons = collectionLatestFile?.map((item) => (
|
||||
<CollectionDropZone key={item.collection.id}
|
||||
{...rest}
|
||||
closeModal={closeUploadModal}
|
||||
showModal={showUploadModal}
|
||||
collectionLatestFile={item}
|
||||
>
|
||||
<Card>
|
||||
<PreviewCard data={item.file} updateUrl={() => { }} forcedEnable />
|
||||
<Card.Text className="text-center">{item.collection.name}</Card.Text>
|
||||
</Card>
|
||||
|
||||
</CollectionDropZone>
|
||||
));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={uploadModalView}
|
||||
onHide={closeUploadModal}
|
||||
dialogClassName="modal-90w"
|
||||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title >
|
||||
{constants.SELECT_COLLECTION}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body style={{ display: "flex", justifyContent: "flex-start", flexWrap: "wrap" }}>
|
||||
<AddCollection
|
||||
{...rest}
|
||||
showUploadModal={showUploadModal}
|
||||
closeUploadModal={closeUploadModal}
|
||||
/>
|
||||
{CollectionIcons}
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default CollectionSelector;
|
|
@ -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>;
|
||||
|
|
32
src/pages/gallery/components/Upload.tsx
Normal file
32
src/pages/gallery/components/Upload.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import React, { useState } from "react"
|
||||
import { UPLOAD_STAGES } from "services/uploadService";
|
||||
import CollectionSelector from "./CollectionSelector"
|
||||
import UploadProgress from "./UploadProgress"
|
||||
|
||||
export default function Upload(props) {
|
||||
const [progressView, setProgressView] = useState(false);
|
||||
const [uploadStage, setUploadStage] = useState<UPLOAD_STAGES>(UPLOAD_STAGES.START);
|
||||
const [fileCounter, setFileCounter] = useState({ current: 0, total: 0 });
|
||||
const [percentComplete, setPercentComplete] = useState(0);
|
||||
const init = () => {
|
||||
setProgressView(false);
|
||||
setUploadStage(UPLOAD_STAGES.START);
|
||||
setFileCounter({ current: 0, total: 0 });
|
||||
setPercentComplete(0);
|
||||
}
|
||||
return (<>
|
||||
<CollectionSelector
|
||||
{...props}
|
||||
setProgressView={setProgressView}
|
||||
progressBarProps={{ setPercentComplete, setFileCounter, setUploadStage }}
|
||||
/>
|
||||
<UploadProgress
|
||||
now={percentComplete}
|
||||
fileCounter={fileCounter}
|
||||
uploadStage={uploadStage}
|
||||
show={progressView}
|
||||
onHide={init}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
12
src/pages/gallery/components/UploadButton.tsx
Normal file
12
src/pages/gallery/components/UploadButton.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
|
||||
function UploadButton({ showModal }) {
|
||||
return (
|
||||
<Button variant='primary' onClick={showModal}>
|
||||
Upload New Photos
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default UploadButton;
|
31
src/pages/gallery/components/UploadProgress.tsx
Normal file
31
src/pages/gallery/components/UploadProgress.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
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 (
|
||||
<Modal
|
||||
{...props}
|
||||
size='lg'
|
||||
aria-labelledby='contained-modal-title-vcenter'
|
||||
centered
|
||||
backdrop="static"
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title id='contained-modal-title-vcenter'>
|
||||
Uploading Files
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{now === 100 ? (
|
||||
<Alert variant='success'>{constants.UPLOAD[3]}</Alert>
|
||||
) : (
|
||||
<>
|
||||
<Alert variant='info'>{constants.UPLOAD[uploadStage]} {fileCounter?.total != 0 ? `${fileCounter?.current} ${constants.OF} ${fileCounter?.total}` : ''}</Alert>
|
||||
<ProgressBar animated now={now} />
|
||||
</>
|
||||
)}
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -2,28 +2,41 @@ import React, { useEffect, useState } from 'react';
|
|||
import { useRouter } from 'next/router';
|
||||
import Spinner from 'react-bootstrap/Spinner';
|
||||
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
|
||||
import { collection, fetchCollections, file, getFile, getFiles, getPreview } from 'services/fileService';
|
||||
import {
|
||||
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,
|
||||
VIDEO,
|
||||
OTHERS
|
||||
}
|
||||
|
||||
interface TimeStampListItem {
|
||||
itemType: ITEM_TYPE,
|
||||
items?: file[],
|
||||
itemStartIndex?: number,
|
||||
date?: string,
|
||||
itemType: ITEM_TYPE;
|
||||
items?: file[];
|
||||
itemStartIndex?: number;
|
||||
date?: string;
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
|
@ -76,47 +89,57 @@ const DateContainer = styled.div`
|
|||
padding: 0 4px;
|
||||
`;
|
||||
|
||||
const PAGE_SIZE = 12;
|
||||
const COLUMNS = 3;
|
||||
|
||||
export default function Gallery() {
|
||||
export default function Gallery(props) {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [collections, setCollections] = useState<collection[]>([])
|
||||
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 } = {};
|
||||
const fetching: { [k: number]: boolean } = {};
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
||||
const token = getData(LS_KEYS.USER).token;
|
||||
if (!key) {
|
||||
router.push("/");
|
||||
router.push('/');
|
||||
}
|
||||
const main = async () => {
|
||||
setLoading(true);
|
||||
const encryptionKey = await getActualKey();
|
||||
const collections = await fetchCollections(token, encryptionKey);
|
||||
const resp = await getFiles("0", token, "100", encryptionKey, collections);
|
||||
await syncWithRemote();
|
||||
setLoading(false);
|
||||
setCollections(collections);
|
||||
setData(resp.map(item => ({
|
||||
...item,
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
})));
|
||||
};
|
||||
main();
|
||||
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>
|
||||
return (
|
||||
<div className='text-center'>
|
||||
<Spinner animation='border' variant='primary' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const updateUrl = (index: number) => (url: string) => {
|
||||
|
@ -125,8 +148,8 @@ export default function Gallery() {
|
|||
msrc: url,
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
}
|
||||
if (data[index].metadata.fileType === 1 && !data[index].html) {
|
||||
};
|
||||
if (data[index].metadata.fileType === FILE_TYPE.VIDEO && !data[index].html) {
|
||||
data[index].html = `
|
||||
<div class="video-loading">
|
||||
<img src="${url}" />
|
||||
|
@ -137,11 +160,11 @@ export default function Gallery() {
|
|||
`;
|
||||
delete data[index].src;
|
||||
}
|
||||
if (data[index].metadata.fileType === 0 && !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] = {
|
||||
|
@ -149,8 +172,8 @@ export default function Gallery() {
|
|||
src: url,
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
}
|
||||
if (data[index].metadata.fileType === 1) {
|
||||
};
|
||||
if (data[index].metadata.fileType === FILE_TYPE.VIDEO) {
|
||||
data[index].html = `
|
||||
<video controls>
|
||||
<source src="${url}" />
|
||||
|
@ -160,11 +183,12 @@ export default function Gallery() {
|
|||
delete data[index].src;
|
||||
}
|
||||
setData(data);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
}
|
||||
// syncWithRemote();
|
||||
};
|
||||
|
||||
const onThumbnailClick = (index: number) => () => {
|
||||
setOptions({
|
||||
|
@ -172,16 +196,18 @@ export default function Gallery() {
|
|||
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)}
|
||||
/>);
|
||||
}
|
||||
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;
|
||||
|
@ -205,7 +231,7 @@ export default function Gallery() {
|
|||
fetching[item.dataIndex] = true;
|
||||
const url = await getFile(token, item);
|
||||
updateSrcUrl(item.dataIndex, url);
|
||||
if (item.metadata.fileType === 1) {
|
||||
if (item.metadata.fileType === FILE_TYPE.VIDEO) {
|
||||
item.html = `
|
||||
<video width="320" height="240" controls>
|
||||
<source src="${url}" />
|
||||
|
@ -225,43 +251,58 @@ export default function Gallery() {
|
|||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const selectCollection = (id?: string) => {
|
||||
const href = `/gallery?collection=${id || ''}`;
|
||||
router.push(href, undefined, { shallow: true });
|
||||
}
|
||||
};
|
||||
|
||||
const 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;
|
||||
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;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
const isSameDay = (first, second) => {
|
||||
return first.getFullYear() === second.getFullYear() &&
|
||||
return (
|
||||
first.getFullYear() === second.getFullYear() &&
|
||||
first.getMonth() === second.getMonth() &&
|
||||
first.getDate() === second.getDate();
|
||||
}
|
||||
first.getDate() === second.getDate()
|
||||
);
|
||||
};
|
||||
|
||||
return (<>
|
||||
<Collections
|
||||
collections={collections}
|
||||
selected={router.query.collection?.toString()}
|
||||
selectCollection={selectCollection}
|
||||
/>
|
||||
{
|
||||
filteredData.length
|
||||
? <Container>
|
||||
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;
|
||||
|
@ -279,13 +320,18 @@ export default function Gallery() {
|
|||
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;
|
||||
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'
|
||||
day: 'numeric',
|
||||
});
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.TIME,
|
||||
|
@ -307,34 +353,46 @@ export default function Gallery() {
|
|||
itemType: ITEM_TYPE.TILE,
|
||||
items: [item],
|
||||
itemStartIndex: index,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<List
|
||||
itemSize={(index) => timeStampList[index].itemType === ITEM_TYPE.TIME ? 30 : 200}
|
||||
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>);
|
||||
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
|
||||
|
@ -343,12 +401,15 @@ export default function Gallery() {
|
|||
options={options}
|
||||
onClose={handleClose}
|
||||
gettingData={getSlideData}
|
||||
favItemIds={favItemIds}
|
||||
setFavItemIds={setFavItemIds}
|
||||
/>
|
||||
</Container>
|
||||
: <DeadCenter>
|
||||
<SadFace height={100} width={100} />
|
||||
<div>No content found!</div>
|
||||
</DeadCenter>
|
||||
}
|
||||
</>);
|
||||
) : (
|
||||
<DeadCenter>
|
||||
<div>{constants.NOTHING_HERE}</div>
|
||||
</DeadCenter>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
|||
import { useRouter } from 'next/router';
|
||||
import { getKey, SESSION_KEYS, setKey } from 'utils/storage/sessionStorage';
|
||||
import * as Comlink from "comlink";
|
||||
import { keyEncryptionResult } from 'services/uploadService';
|
||||
|
||||
const CryptoWorker: any = typeof window !== 'undefined'
|
||||
&& Comlink.wrap(new Worker("worker/crypto.worker.js", { type: 'module' }));
|
||||
|
@ -51,30 +52,30 @@ export default function Generate() {
|
|||
const { passphrase, confirm } = values;
|
||||
if (passphrase === confirm) {
|
||||
const cryptoWorker = await new CryptoWorker();
|
||||
const key = await cryptoWorker.generateMasterKey();
|
||||
const kekSalt = await cryptoWorker.generateSaltToDeriveKey();
|
||||
const kek = await cryptoWorker.deriveKey(
|
||||
await cryptoWorker.fromString(passphrase), kekSalt);
|
||||
const kekHash = await cryptoWorker.hash(kek);
|
||||
const encryptedKeyAttributes = await cryptoWorker.encrypt(key, kek);
|
||||
const key: string = await cryptoWorker.generateMasterKey();
|
||||
const kekSalt: string = await cryptoWorker.generateSaltToDeriveKey();
|
||||
const kek: string = await cryptoWorker.deriveKey(passphrase, kekSalt);
|
||||
const kekHash: string = await cryptoWorker.hash(kek);
|
||||
const encryptedKeyAttributes: keyEncryptionResult = await cryptoWorker.encryptToB64(key, kek);
|
||||
const keyPair = await cryptoWorker.generateKeyPair();
|
||||
const encryptedKeyPairAttributes = await cryptoWorker.encrypt(keyPair.privateKey, key);
|
||||
const encryptedKeyPairAttributes: keyEncryptionResult = await cryptoWorker.encryptToB64(keyPair.privateKey, key);
|
||||
|
||||
const keyAttributes = {
|
||||
kekSalt: await cryptoWorker.toB64(kekSalt),
|
||||
kekSalt,
|
||||
kekHash: kekHash,
|
||||
encryptedKey: await cryptoWorker.toB64(encryptedKeyAttributes.encryptedData),
|
||||
keyDecryptionNonce: await cryptoWorker.toB64(encryptedKeyAttributes.nonce),
|
||||
publicKey: await cryptoWorker.toB64(keyPair.publicKey),
|
||||
encryptedSecretKey: await cryptoWorker.toB64(encryptedKeyPairAttributes.encryptedData),
|
||||
secretKeyDecryptionNonce: await cryptoWorker.toB64(encryptedKeyPairAttributes.nonce)
|
||||
encryptedKey: encryptedKeyAttributes.encryptedData,
|
||||
keyDecryptionNonce: encryptedKeyAttributes.nonce,
|
||||
publicKey: keyPair.publicKey,
|
||||
encryptedSecretKey: encryptedKeyPairAttributes.encryptedData,
|
||||
secretKeyDecryptionNonce: encryptedKeyPairAttributes.nonce
|
||||
};
|
||||
await putAttributes(token, getData(LS_KEYS.USER).name, keyAttributes);
|
||||
setData(LS_KEYS.KEY_ATTRIBUTES, keyAttributes);
|
||||
|
||||
const sessionKeyAttributes = await cryptoWorker.encrypt(key);
|
||||
const sessionKey = await cryptoWorker.toB64(sessionKeyAttributes.key);
|
||||
const sessionNonce = await cryptoWorker.toB64(sessionKeyAttributes.nonce);
|
||||
const encryptionKey = await cryptoWorker.toB64(sessionKeyAttributes.encryptedData);
|
||||
const sessionKeyAttributes = await cryptoWorker.encryptToB64(key);
|
||||
const sessionKey = sessionKeyAttributes.key;
|
||||
const sessionNonce = sessionKeyAttributes.nonce;
|
||||
const encryptionKey = sessionKeyAttributes.encryptedData;
|
||||
setKey(SESSION_KEYS.ENCRYPTION_KEY, { encryptionKey });
|
||||
setData(LS_KEYS.SESSION, { sessionKey, sessionNonce });
|
||||
router.push('/gallery');
|
||||
|
|
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,11 +1,12 @@
|
|||
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 { getEndpoint } from 'utils/common/apiUtil';
|
||||
import HTTPService from './HTTPService';
|
||||
import * as Comlink from 'comlink';
|
||||
import localForage from 'localforage';
|
||||
import { collection } from './collectionService';
|
||||
|
||||
const CryptoWorker: any = typeof window !== 'undefined'
|
||||
&& Comlink.wrap(new Worker("worker/crypto.worker.js", { type: 'module' }));
|
||||
const CryptoWorker: any =
|
||||
typeof window !== 'undefined' &&
|
||||
Comlink.wrap(new Worker('worker/crypto.worker.js', { type: 'module' }));
|
||||
const ENDPOINT = getEndpoint();
|
||||
|
||||
localForage.config({
|
||||
|
@ -16,11 +17,11 @@ localForage.config({
|
|||
});
|
||||
|
||||
export interface fileAttribute {
|
||||
encryptedData: string;
|
||||
encryptedData: Uint8Array | string;
|
||||
decryptionHeader: string;
|
||||
creationTime: number;
|
||||
fileType: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface user {
|
||||
id: number;
|
||||
|
@ -28,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;
|
||||
|
@ -48,7 +38,7 @@ export interface file {
|
|||
metadata: fileAttribute;
|
||||
encryptedKey: string;
|
||||
keyDecryptionNonce: string;
|
||||
key: Uint8Array;
|
||||
key: string;
|
||||
src: string;
|
||||
msrc: string;
|
||||
html: string;
|
||||
|
@ -56,119 +46,149 @@ export interface file {
|
|||
h: number;
|
||||
isDeleted: boolean;
|
||||
dataIndex: number;
|
||||
};
|
||||
|
||||
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
|
||||
};
|
||||
updationTime: number;
|
||||
}
|
||||
|
||||
const getCollections = async (token: string, sinceTime: string, key: Uint8Array): Promise<collection[]> => {
|
||||
const resp = await HTTPService.get(`${ENDPOINT}/collections`, {
|
||||
'token': token,
|
||||
'sinceTime': sinceTime,
|
||||
});
|
||||
|
||||
const promises: Promise<collection>[] = resp.data.collections.map(
|
||||
(collection: collection) => getCollectionKey(collection, key));
|
||||
return await Promise.all(promises);
|
||||
|
||||
export const fetchData = async (token, collections) => {
|
||||
const resp = await fetchFiles(
|
||||
token,
|
||||
collections
|
||||
);
|
||||
|
||||
return (
|
||||
resp.map((item) => ({
|
||||
...item,
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
export const fetchCollections = async (token: string, key: string) => {
|
||||
const worker = await new CryptoWorker();
|
||||
return getCollections(token, "0", await worker.fromB64(key));
|
||||
}
|
||||
export const fetchFiles = async (
|
||||
token: string,
|
||||
collections: collection[]
|
||||
) => {
|
||||
let files: Array<file> = (await localForage.getItem<file[]>('files')) || [];
|
||||
const fetchedFiles = await getFiles(collections, null, "100", token);
|
||||
|
||||
export const getFiles = async (sinceTime: string, 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;
|
||||
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.map(
|
||||
async (file: file) => {
|
||||
file.key = await worker.decrypt(
|
||||
await worker.fromB64(file.encryptedKey),
|
||||
await worker.fromB64(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.filter(item => !item.isDeleted)
|
||||
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);
|
||||
}
|
||||
return URL.createObjectURL(new Blob([decrypted]));
|
||||
}
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
495
src/services/uploadService.ts
Normal file
495
src/services/uploadService.ts
Normal file
|
@ -0,0 +1,495 @@
|
|||
import { getEndpoint } from 'utils/common/apiUtil';
|
||||
import HTTPService from './HTTPService';
|
||||
import * as Comlink from 'comlink';
|
||||
import EXIF from "exif-js";
|
||||
import { fileAttribute } from './fileService';
|
||||
import { collection, collectionLatestFile } from "./collectionService"
|
||||
import { FILE_TYPE } from 'pages/gallery';
|
||||
const CryptoWorker: any =
|
||||
typeof window !== 'undefined' &&
|
||||
Comlink.wrap(new Worker('worker/crypto.worker.js', { type: 'module' }));
|
||||
const ENDPOINT = getEndpoint();
|
||||
|
||||
|
||||
interface encryptionResult {
|
||||
file: fileAttribute,
|
||||
key: string
|
||||
}
|
||||
export interface keyEncryptionResult {
|
||||
encryptedData: string,
|
||||
key: string,
|
||||
nonce: string,
|
||||
}
|
||||
|
||||
interface uploadURL {
|
||||
url: string,
|
||||
objectKey: string
|
||||
}
|
||||
|
||||
interface FileinMemory {
|
||||
filedata: Uint8Array,
|
||||
thumbnail: Uint8Array,
|
||||
filename: string
|
||||
}
|
||||
|
||||
|
||||
interface encryptedFile {
|
||||
filedata: fileAttribute;
|
||||
thumbnail: fileAttribute;
|
||||
fileKey: keyEncryptionResult;
|
||||
}
|
||||
|
||||
interface objectKey {
|
||||
objectKey: string,
|
||||
decryptionHeader: string
|
||||
}
|
||||
interface objectKeys {
|
||||
file: objectKey
|
||||
thumbnail: objectKey
|
||||
}
|
||||
|
||||
interface uploadFile extends objectKeys {
|
||||
collectionID: string,
|
||||
encryptedKey: string;
|
||||
keyDecryptionNonce: string;
|
||||
metadata?: {
|
||||
encryptedData: string | Uint8Array,
|
||||
decryptionHeader: string
|
||||
}
|
||||
}
|
||||
|
||||
interface UploadFileWithoutMetaData {
|
||||
tempUploadFile: uploadFile,
|
||||
encryptedFileKey: keyEncryptionResult,
|
||||
fileName: string
|
||||
}
|
||||
|
||||
export enum UPLOAD_STAGES {
|
||||
START,
|
||||
ENCRYPTION,
|
||||
UPLOAD,
|
||||
FINISH
|
||||
}
|
||||
|
||||
class UploadService {
|
||||
|
||||
private uploadURLs: uploadURL[];
|
||||
private uploadURLFetchInProgress: Promise<any>;
|
||||
private perStepProgress: number
|
||||
private stepsCompleted: number
|
||||
private totalFilesCount: number
|
||||
private metadataMap: Map<string, Object>;
|
||||
|
||||
public async uploadFiles(recievedFiles: File[], collectionLatestFile: collectionLatestFile, token: string, progressBarProps) {
|
||||
try {
|
||||
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[] = [];
|
||||
recievedFiles.forEach(file => {
|
||||
if (file.type.substr(0, 5) === "image" || file.type.substr(0, 5) === "video")
|
||||
actualFiles.push(file);
|
||||
if (file.name.slice(-4) == "json")
|
||||
metadataFiles.push(file);
|
||||
});
|
||||
this.totalFilesCount = actualFiles.length;
|
||||
this.perStepProgress = 100 / (3 * actualFiles.length);
|
||||
|
||||
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 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 completeUploadFile;
|
||||
}));
|
||||
|
||||
progressBarProps.setUploadStage(UPLOAD_STAGES.UPLOAD);
|
||||
await Promise.all(completeUploadFiles.map(async (uploadFile: uploadFile) => {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private changeProgressBarProps({ setPercentComplete, setFileCounter }) {
|
||||
this.stepsCompleted++;
|
||||
const fileCompleted = this.stepsCompleted % this.totalFilesCount;
|
||||
setFileCounter({ current: fileCompleted, total: this.totalFilesCount });
|
||||
setPercentComplete(this.perStepProgress * this.stepsCompleted);
|
||||
}
|
||||
|
||||
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?.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 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 result: encryptedFile = {
|
||||
filedata: encryptedFiledata,
|
||||
thumbnail: encryptedThumbnail,
|
||||
fileKey: encryptedKey
|
||||
};
|
||||
return result;
|
||||
}
|
||||
catch (e) {
|
||||
console.log("Error encrypting files " + e);
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
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)
|
||||
|
||||
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 getuploadFile(collection: collection, encryptedKey: keyEncryptionResult, objectKeys: objectKeys): uploadFile {
|
||||
const uploadFile: uploadFile = {
|
||||
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 });
|
||||
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
console.log("upload Files Failed " + e);
|
||||
}
|
||||
}
|
||||
|
||||
private async updateMetadata(recievedFile: File) {
|
||||
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);
|
||||
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private async getUint8ArrayView(file): Promise<Uint8Array> {
|
||||
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)
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("error readinf file to bytearray " + e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private async getUploadURL(token: string, count: number) {
|
||||
if (this.uploadURLs.length == 0) {
|
||||
await this.fetchUploadURLs(token, count);
|
||||
}
|
||||
return this.uploadURLs.pop();
|
||||
}
|
||||
|
||||
private async fetchUploadURLs(token: string, count: number): Promise<void> {
|
||||
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"]);
|
||||
}
|
||||
return this.uploadURLFetchInProgress;
|
||||
} catch (e) {
|
||||
console.log("fetch upload-url failed " + e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private async putFile(fileUploadURL: uploadURL, file: Uint8Array | string): Promise<string> {
|
||||
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) {
|
||||
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)
|
||||
return null;
|
||||
let dateString: string = exifData.DateTimeOriginal;
|
||||
var parts = dateString.split(' ')[0].split(":");
|
||||
var date = new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
|
||||
return date.getTime() * 1000;
|
||||
}
|
||||
|
||||
private getLocation(exifData) {
|
||||
|
||||
if (!exifData.GPSLatitude)
|
||||
return null;
|
||||
var latDegree = exifData.GPSLatitude[0].numerator;
|
||||
var latMinute = exifData.GPSLatitude[1].numerator;
|
||||
var latSecond = exifData.GPSLatitude[2].numerator;
|
||||
var latDirection = exifData.GPSLatitudeRef;
|
||||
|
||||
var latFinal = this.convertDMSToDD(latDegree, latMinute, latSecond, latDirection);
|
||||
|
||||
// Calculate longitude decimal
|
||||
var lonDegree = exifData.GPSLongitude[0].numerator;
|
||||
var lonMinute = exifData.GPSLongitude[1].numerator;
|
||||
var lonSecond = exifData.GPSLongitude[2].numerator;
|
||||
var lonDirection = exifData.GPSLongitudeRef;
|
||||
|
||||
var lonFinal = this.convertDMSToDD(lonDegree, lonMinute, lonSecond, lonDirection);
|
||||
|
||||
return { latitude: latFinal * 1.0, longitude: lonFinal * 1.0 };
|
||||
}
|
||||
|
||||
private convertDMSToDD(degrees, minutes, seconds, direction) {
|
||||
|
||||
var dd = degrees + (minutes / 60) + (seconds / 3600);
|
||||
|
||||
if (direction == "S" || direction == "W") {
|
||||
dd = dd * -1;
|
||||
}
|
||||
|
||||
return dd;
|
||||
}
|
||||
}
|
||||
|
||||
export default new UploadService();
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -6,9 +6,15 @@ const CryptoWorker: any = typeof window !== 'undefined'
|
|||
&& Comlink.wrap(new Worker("worker/crypto.worker.js", { type: 'module' }));
|
||||
|
||||
export const getActualKey = async () => {
|
||||
const cryptoWorker = await new CryptoWorker();
|
||||
const encryptedKey = getKey(SESSION_KEYS.ENCRYPTION_KEY).encryptionKey;
|
||||
const session = getData(LS_KEYS.SESSION);
|
||||
const key = await cryptoWorker.decryptToB64(encryptedKey, session.sessionNonce, session.sessionKey);
|
||||
if (session == null)
|
||||
return;
|
||||
const cryptoWorker = await new CryptoWorker();
|
||||
const encryptedKey = getKey(SESSION_KEYS.ENCRYPTION_KEY)?.encryptionKey;
|
||||
const key: string = await cryptoWorker.decryptB64(encryptedKey, session.sessionNonce, session.sessionKey);
|
||||
return key;
|
||||
}
|
||||
|
||||
export const getToken = () => {
|
||||
return getData(LS_KEYS.USER)?.token;
|
||||
}
|
||||
|
|
|
@ -2,16 +2,16 @@ import sodium from 'libsodium-wrappers';
|
|||
|
||||
const encryptionChunkSize = 4 * 1024 * 1024;
|
||||
|
||||
export async function decryptChaChaOneShot(data: Uint8Array, header: Uint8Array, key: Uint8Array) {
|
||||
export async function decryptChaChaOneShot(data: Uint8Array, header: Uint8Array, key: string) {
|
||||
await sodium.ready;
|
||||
const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull(header, key);
|
||||
const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull(header, await fromB64(key));
|
||||
const pullResult = sodium.crypto_secretstream_xchacha20poly1305_pull(pullState, data, null);
|
||||
return pullResult.message;
|
||||
}
|
||||
|
||||
export async function decryptChaCha(data: Uint8Array, header: Uint8Array, key: Uint8Array) {
|
||||
export async function decryptChaCha(data: Uint8Array, header: Uint8Array, key: string) {
|
||||
await sodium.ready;
|
||||
const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull(header, key);
|
||||
const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull(header, await fromB64(key));
|
||||
const decryptionChunkSize =
|
||||
encryptionChunkSize + sodium.crypto_secretstream_xchacha20poly1305_ABYTES;
|
||||
var bytesRead = 0;
|
||||
|
@ -33,41 +33,97 @@ export async function decryptChaCha(data: Uint8Array, header: Uint8Array, key: U
|
|||
return Uint8Array.from(decryptedData);
|
||||
}
|
||||
|
||||
export async function encryptToB64(data: string, key: string) {
|
||||
export async function encryptChaChaOneShot(data: Uint8Array, key?: string) {
|
||||
await sodium.ready;
|
||||
var bKey: Uint8Array;
|
||||
if (key == null) {
|
||||
bKey = sodium.crypto_secretbox_keygen();
|
||||
} else {
|
||||
bKey = await fromB64(key)
|
||||
}
|
||||
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
|
||||
const encryptedData = sodium.crypto_secretbox_easy(data, nonce, bKey);
|
||||
|
||||
const uintkey: Uint8Array = key ? await fromB64(key) : sodium.crypto_secretstream_xchacha20poly1305_keygen();
|
||||
let initPushResult = sodium.crypto_secretstream_xchacha20poly1305_init_push(uintkey);
|
||||
let [pushState, header] = [initPushResult.state, initPushResult.header];
|
||||
|
||||
const pushResult = sodium.crypto_secretstream_xchacha20poly1305_push(pushState, data, null, sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL);
|
||||
return {
|
||||
encryptedData: await toB64(encryptedData),
|
||||
key: await toB64(bKey),
|
||||
nonce: await toB64(nonce),
|
||||
key: await toB64(uintkey), file: {
|
||||
encryptedData: pushResult,
|
||||
decryptionHeader: await toB64(header),
|
||||
creationTime: Date.now(),
|
||||
fileType: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function decryptToB64(data: string, nonce: string, key: string) {
|
||||
export async function encryptChaCha(data: Uint8Array, key?: string) {
|
||||
await sodium.ready;
|
||||
|
||||
const uintkey: Uint8Array = key ? await fromB64(key) : sodium.crypto_secretstream_xchacha20poly1305_keygen();
|
||||
|
||||
let initPushResult = sodium.crypto_secretstream_xchacha20poly1305_init_push(uintkey);
|
||||
let [pushState, header] = [initPushResult.state, initPushResult.header];
|
||||
let bytesRead = 0;
|
||||
let tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
|
||||
|
||||
let encryptedData = [];
|
||||
|
||||
while (tag !== sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL) {
|
||||
let chunkSize = encryptionChunkSize;
|
||||
if (bytesRead + chunkSize >= data.length) {
|
||||
chunkSize = data.length - bytesRead;
|
||||
tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL;
|
||||
}
|
||||
|
||||
const buffer = data.slice(bytesRead, bytesRead + chunkSize);
|
||||
bytesRead += chunkSize;
|
||||
const pushResult = sodium.crypto_secretstream_xchacha20poly1305_push(pushState, buffer, null, tag);
|
||||
for (var index = 0; index < pushResult.length; index++) {
|
||||
encryptedData.push(pushResult[index]);
|
||||
}
|
||||
}
|
||||
return {
|
||||
key: await toB64(uintkey), file: {
|
||||
encryptedData: new Uint8Array(encryptedData),
|
||||
decryptionHeader: await toB64(header),
|
||||
creationTime: Date.now(),
|
||||
fileType: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function encryptToB64(data: string, key?: string) {
|
||||
await sodium.ready;
|
||||
const encrypted = await encrypt(await fromB64(data), (key ? await fromB64(key) : null));
|
||||
|
||||
return {
|
||||
encryptedData: await toB64(encrypted.encryptedData),
|
||||
key: await toB64(encrypted.key),
|
||||
nonce: await toB64(encrypted.nonce),
|
||||
}
|
||||
}
|
||||
|
||||
export async function decryptB64(data: string, nonce: string, key: string) {
|
||||
await sodium.ready;
|
||||
const decrypted = await decrypt(await fromB64(data),
|
||||
await fromB64(nonce),
|
||||
await fromB64(key))
|
||||
await fromB64(key));
|
||||
|
||||
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;
|
||||
if (key == null) {
|
||||
key = sodium.crypto_secretbox_keygen();
|
||||
}
|
||||
const uintkey: Uint8Array = key ? key : sodium.crypto_secretbox_keygen();
|
||||
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
|
||||
const encryptedData = sodium.crypto_secretbox_easy(data, nonce, key);
|
||||
const encryptedData = sodium.crypto_secretbox_easy(data, nonce, uintkey);
|
||||
return {
|
||||
encryptedData: encryptedData,
|
||||
key: key,
|
||||
key: uintkey,
|
||||
nonce: nonce,
|
||||
}
|
||||
}
|
||||
|
@ -77,55 +133,65 @@ export async function decrypt(data: Uint8Array, nonce: Uint8Array, key: Uint8Arr
|
|||
return sodium.crypto_secretbox_open_easy(data, nonce, key);
|
||||
}
|
||||
|
||||
export async function verifyHash(hash: string, input: Uint8Array) {
|
||||
export async function verifyHash(hash: string, input: string) {
|
||||
await sodium.ready;
|
||||
return sodium.crypto_pwhash_str_verify(hash, input);
|
||||
return sodium.crypto_pwhash_str_verify(hash, await fromB64(input));
|
||||
}
|
||||
|
||||
export async function hash(input: string | Uint8Array) {
|
||||
export async function hash(input: string) {
|
||||
await sodium.ready;
|
||||
return sodium.crypto_pwhash_str(
|
||||
input,
|
||||
await fromB64(input),
|
||||
sodium.crypto_pwhash_OPSLIMIT_SENSITIVE,
|
||||
sodium.crypto_pwhash_MEMLIMIT_MODERATE,
|
||||
);
|
||||
}
|
||||
|
||||
export async function deriveKey(passphrase: Uint8Array, salt: Uint8Array) {
|
||||
export async function deriveKey(passphrase: string, salt: string) {
|
||||
await sodium.ready;
|
||||
return sodium.crypto_pwhash(
|
||||
return await toB64(sodium.crypto_pwhash(
|
||||
sodium.crypto_secretbox_KEYBYTES,
|
||||
passphrase,
|
||||
salt,
|
||||
await fromString(passphrase),
|
||||
await fromB64(salt),
|
||||
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
|
||||
sodium.crypto_pwhash_ALG_DEFAULT,
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
export async function generateMasterKey() {
|
||||
await sodium.ready;
|
||||
return sodium.crypto_kdf_keygen();
|
||||
return await toB64(sodium.crypto_kdf_keygen());
|
||||
}
|
||||
|
||||
export async function generateSaltToDeriveKey() {
|
||||
await sodium.ready;
|
||||
return sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
|
||||
return await toB64(sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES));
|
||||
}
|
||||
|
||||
export async function generateKeyPair() {
|
||||
await sodium.ready;
|
||||
return sodium.crypto_box_keypair();
|
||||
const keyPair: sodium.KeyPair = sodium.crypto_box_keypair();
|
||||
return { privateKey: await toB64(keyPair.privateKey), publicKey: await toB64(keyPair.publicKey) }
|
||||
}
|
||||
|
||||
export async function boxSealOpen(input: Uint8Array, publicKey: Uint8Array, secretKey: Uint8Array) {
|
||||
export async function boxSealOpen(input: string, publicKey: string, secretKey: string) {
|
||||
await sodium.ready;
|
||||
return sodium.crypto_box_seal_open(input, publicKey, secretKey);
|
||||
return await toB64(sodium.crypto_box_seal_open(await fromB64(input), await fromB64(publicKey), await fromB64(secretKey)));
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
@ -24,6 +24,32 @@ export class Crypto {
|
|||
key);
|
||||
}
|
||||
|
||||
async encryptMetadata(metadata, key) {
|
||||
const encodedMetadata = new TextEncoder().encode(JSON.stringify(metadata));
|
||||
|
||||
const { file: encryptedMetadata } = await libsodium.encryptChaChaOneShot(
|
||||
encodedMetadata,
|
||||
key);
|
||||
const { encryptedData, ...other } = encryptedMetadata
|
||||
return {
|
||||
file: {
|
||||
encryptedData: await libsodium.toB64(encryptedData),
|
||||
...other
|
||||
},
|
||||
key
|
||||
};
|
||||
}
|
||||
|
||||
async encryptThumbnail(fileData, key) {
|
||||
return libsodium.encryptChaChaOneShot(
|
||||
fileData,
|
||||
key);
|
||||
}
|
||||
|
||||
async encryptFile(fileData, key) {
|
||||
return libsodium.encryptChaCha(fileData, key);
|
||||
}
|
||||
|
||||
async encrypt(data, key) {
|
||||
return libsodium.encrypt(data, key);
|
||||
}
|
||||
|
@ -44,8 +70,16 @@ export class Crypto {
|
|||
return libsodium.deriveKey(passphrase, salt);
|
||||
}
|
||||
|
||||
async decryptToB64(encryptedKey, sessionNonce, sessionKey) {
|
||||
return libsodium.decryptToB64(encryptedKey, sessionNonce, sessionKey)
|
||||
async decryptB64(data, nonce, key) {
|
||||
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);
|
||||
}
|
||||
|
||||
async generateMasterKey() {
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
39
yarn.lock
39
yarn.lock
|
@ -1673,6 +1673,11 @@ atob@^2.1.2:
|
|||
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
|
||||
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
|
||||
|
||||
attr-accept@^2.2.1:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b"
|
||||
integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==
|
||||
|
||||
axios@^0.20.0:
|
||||
version "0.20.0"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.20.0.tgz#057ba30f04884694993a8cd07fa394cff11c50bd"
|
||||
|
@ -2848,6 +2853,11 @@ execa@^4.0.2:
|
|||
signal-exit "^3.0.2"
|
||||
strip-final-newline "^2.0.0"
|
||||
|
||||
exif-js@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/exif-js/-/exif-js-2.3.0.tgz#9d10819bf571f873813e7640241255ab9ce1a814"
|
||||
integrity sha1-nRCBm/Vx+HOBPnZAJBJVq5zhqBQ=
|
||||
|
||||
expand-brackets@^2.1.4:
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
|
||||
|
@ -2948,6 +2958,13 @@ figgy-pudding@^3.5.1:
|
|||
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
|
||||
integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==
|
||||
|
||||
file-selector@^0.2.2:
|
||||
version "0.2.4"
|
||||
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.2.4.tgz#7b98286f9dbb9925f420130ea5ed0a69238d4d80"
|
||||
integrity sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==
|
||||
dependencies:
|
||||
tslib "^2.0.3"
|
||||
|
||||
file-uri-to-path@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
|
||||
|
@ -4923,6 +4940,15 @@ react-dom@16.13.1:
|
|||
prop-types "^15.6.2"
|
||||
scheduler "^0.19.1"
|
||||
|
||||
react-dropzone@^11.2.4:
|
||||
version "11.3.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.3.0.tgz#516561c5003e0c0f7d63bd5621f410b1b3496ab3"
|
||||
integrity sha512-5ffIOi5Uf1X52m4fN8QdcRuAX88nQPfmx6HTTIfF9I3W9Ss1SvRDl/ruZmFf53K7+g3TSaIgVw6a9EK7XoDwHw==
|
||||
dependencies:
|
||||
attr-accept "^2.2.1"
|
||||
file-selector "^0.2.2"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
react-fast-compare@^2.0.1:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
||||
|
@ -5851,6 +5877,11 @@ tslib@^1.10.0, tslib@^1.9.0:
|
|||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
|
||||
integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
|
||||
|
||||
tslib@^2.0.3:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
|
||||
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
|
||||
|
||||
tty-browserify@0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
|
||||
|
@ -5884,10 +5915,10 @@ typedarray@^0.0.6:
|
|||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
|
||||
|
||||
typescript@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2"
|
||||
integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ==
|
||||
typescript@^4.1.3:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7"
|
||||
integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==
|
||||
|
||||
uncontrollable@^7.0.0:
|
||||
version "7.1.1"
|
||||
|
|
Loading…
Add table
Reference in a new issue