commit
896a450762
|
@ -39,7 +39,6 @@ const Overlay = styled.div`
|
|||
type Props = React.PropsWithChildren<{
|
||||
getRootProps: any;
|
||||
getInputProps: any;
|
||||
showCollectionSelector;
|
||||
}>;
|
||||
|
||||
export default function FullScreenDropZone(props: Props) {
|
||||
|
@ -58,10 +57,6 @@ export default function FullScreenDropZone(props: Props) {
|
|||
<DropDiv
|
||||
{...props.getRootProps({
|
||||
onDragEnter,
|
||||
onDrop: (e) => {
|
||||
e.preventDefault();
|
||||
props.showCollectionSelector();
|
||||
},
|
||||
})}>
|
||||
<input {...props.getInputProps()} />
|
||||
{isDragActive && (
|
||||
|
|
23
src/components/icons/MoveIcon.tsx
Normal file
23
src/components/icons/MoveIcon.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function MoveIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={props.height}
|
||||
viewBox={props.viewBox}
|
||||
width={props.width}>
|
||||
<path
|
||||
d="M13.025 1l-2.847 2.828 6.176 6.176h-16.354v3.992h16.354l-6.176 6.176 2.847 2.828 10.975-11z"
|
||||
strokeWidth="2"
|
||||
stroke="black"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
MoveIcon.defaultProps = {
|
||||
height: 24,
|
||||
width: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Modal } from 'react-bootstrap';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
|
@ -23,6 +23,7 @@ export interface CollectionSelectorAttributes {
|
|||
callback: (collection: Collection) => void;
|
||||
showNextModal: () => void;
|
||||
title: string;
|
||||
fromCollection?: number;
|
||||
}
|
||||
export type SetCollectionSelectorAttributes = React.Dispatch<
|
||||
React.SetStateAction<CollectionSelectorAttributes>
|
||||
|
@ -45,30 +46,46 @@ function CollectionSelector({
|
|||
collectionsAndTheirLatestFile,
|
||||
...props
|
||||
}: Props) {
|
||||
const [collectionToShow, setCollectionToShow] = useState<
|
||||
CollectionAndItsLatestFile[]
|
||||
>([]);
|
||||
useEffect(() => {
|
||||
if (!attributes) {
|
||||
return;
|
||||
}
|
||||
const collectionsOtherThanFrom = collectionsAndTheirLatestFile?.filter(
|
||||
(item) => !(item.collection.id === attributes.fromCollection)
|
||||
);
|
||||
if (collectionsOtherThanFrom.length === 0) {
|
||||
props.onHide();
|
||||
attributes.showNextModal();
|
||||
} else {
|
||||
setCollectionToShow(collectionsOtherThanFrom);
|
||||
}
|
||||
}, [props.show]);
|
||||
|
||||
if (!attributes) {
|
||||
return <Modal />;
|
||||
}
|
||||
const CollectionIcons: JSX.Element[] = collectionsAndTheirLatestFile?.map(
|
||||
(item) => (
|
||||
<CollectionIcon
|
||||
key={item.collection.id}
|
||||
onClick={() => {
|
||||
attributes.callback(item.collection);
|
||||
props.onHide();
|
||||
}}>
|
||||
<CollectionCard>
|
||||
<PreviewCard
|
||||
file={item.file}
|
||||
updateUrl={() => {}}
|
||||
forcedEnable
|
||||
/>
|
||||
<Card.Text className="text-center">
|
||||
{item.collection.name}
|
||||
</Card.Text>
|
||||
</CollectionCard>
|
||||
</CollectionIcon>
|
||||
)
|
||||
);
|
||||
const CollectionIcons: JSX.Element[] = collectionToShow?.map((item) => (
|
||||
<CollectionIcon
|
||||
key={item.collection.id}
|
||||
onClick={() => {
|
||||
attributes.callback(item.collection);
|
||||
props.onHide();
|
||||
}}>
|
||||
<CollectionCard>
|
||||
<PreviewCard
|
||||
file={item.file}
|
||||
updateUrl={() => {}}
|
||||
forcedEnable
|
||||
/>
|
||||
<Card.Text className="text-center">
|
||||
{item.collection.name}
|
||||
</Card.Text>
|
||||
</CollectionCard>
|
||||
</CollectionIcon>
|
||||
));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
|
|
@ -8,15 +8,19 @@ import CrossIcon from 'components/icons/CrossIcon';
|
|||
import AddIcon from 'components/icons/AddIcon';
|
||||
import { IconButton } from 'components/Container';
|
||||
import constants from 'utils/strings/constants';
|
||||
import MoveIcon from 'components/icons/MoveIcon';
|
||||
import { COLLECTION_OPS_TYPE } from 'utils/collection';
|
||||
|
||||
interface Props {
|
||||
addToCollectionHelper: (collectionName, collection) => void;
|
||||
showCreateCollectionModal: () => void;
|
||||
moveToCollectionHelper: (collectionName, collection) => void;
|
||||
showCreateCollectionModal: (opsType: COLLECTION_OPS_TYPE) => () => void;
|
||||
setDialogMessage: SetDialogMessage;
|
||||
setCollectionSelectorAttributes: SetCollectionSelectorAttributes;
|
||||
deleteFileHelper: () => void;
|
||||
count: number;
|
||||
clearSelection: () => void;
|
||||
activeCollection: number;
|
||||
}
|
||||
|
||||
const SelectionBar = styled(Navbar)`
|
||||
|
@ -35,18 +39,21 @@ const SelectionContainer = styled.div`
|
|||
|
||||
const SelectedFileOptions = ({
|
||||
addToCollectionHelper,
|
||||
moveToCollectionHelper,
|
||||
showCreateCollectionModal,
|
||||
setDialogMessage,
|
||||
setCollectionSelectorAttributes,
|
||||
deleteFileHelper,
|
||||
count,
|
||||
clearSelection,
|
||||
activeCollection,
|
||||
}: Props) => {
|
||||
const addToCollection = () =>
|
||||
setCollectionSelectorAttributes({
|
||||
callback: (collection) => addToCollectionHelper(null, collection),
|
||||
showNextModal: showCreateCollectionModal,
|
||||
showNextModal: showCreateCollectionModal(COLLECTION_OPS_TYPE.ADD),
|
||||
title: constants.ADD_TO_COLLECTION,
|
||||
fromCollection: activeCollection,
|
||||
});
|
||||
|
||||
const deleteHandler = () =>
|
||||
|
@ -62,6 +69,15 @@ const SelectedFileOptions = ({
|
|||
close: { text: constants.CANCEL },
|
||||
});
|
||||
|
||||
const moveToCollection = () => {
|
||||
setCollectionSelectorAttributes({
|
||||
callback: (collection) => moveToCollectionHelper(null, collection),
|
||||
showNextModal: showCreateCollectionModal(COLLECTION_OPS_TYPE.MOVE),
|
||||
title: constants.MOVE_TO_COLLECTION,
|
||||
fromCollection: activeCollection,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<SelectionBar>
|
||||
<SelectionContainer>
|
||||
|
@ -72,6 +88,11 @@ const SelectedFileOptions = ({
|
|||
{count} {constants.SELECTED}
|
||||
</div>
|
||||
</SelectionContainer>
|
||||
{activeCollection !== 0 && (
|
||||
<IconButton onClick={moveToCollection}>
|
||||
<MoveIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
<IconButton onClick={addToCollection}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
|
|
|
@ -45,7 +45,6 @@ import EnteSpinner from 'components/EnteSpinner';
|
|||
import { LoadingOverlay } from 'components/LoadingOverlay';
|
||||
import PhotoFrame from 'components/PhotoFrame';
|
||||
import { getSelectedFileIds, sortFilesIntoCollections } from 'utils/file';
|
||||
import { addFilesToCollection } from 'utils/collection';
|
||||
import SearchBar, { DateValue } from 'components/SearchBar';
|
||||
import { Bbox } from 'services/searchService';
|
||||
import SelectedFileOptions from 'components/pages/gallery/SelectedFileOptions';
|
||||
|
@ -63,6 +62,11 @@ import Collections from 'components/pages/gallery/Collections';
|
|||
import { AppContext } from 'pages/_app';
|
||||
import { CustomError, ServerErrorCodes } from 'utils/common/errorUtil';
|
||||
import { PAGES } from 'types';
|
||||
import {
|
||||
copyOrMoveFromCollection,
|
||||
COLLECTION_OPS_TYPE,
|
||||
} from 'utils/collection';
|
||||
import { logError } from 'utils/sentry';
|
||||
|
||||
export const DeadCenter = styled.div`
|
||||
flex: 1;
|
||||
|
@ -295,7 +299,8 @@ export default function Gallery() {
|
|||
) => {
|
||||
loadingBar.current?.continuousStart();
|
||||
try {
|
||||
await addFilesToCollection(
|
||||
await copyOrMoveFromCollection(
|
||||
COLLECTION_OPS_TYPE.ADD,
|
||||
setCollectionSelectorView,
|
||||
selected,
|
||||
files,
|
||||
|
@ -315,14 +320,68 @@ export default function Gallery() {
|
|||
}
|
||||
};
|
||||
|
||||
const showCreateCollectionModal = () =>
|
||||
setCollectionNamerAttributes({
|
||||
title: constants.CREATE_COLLECTION,
|
||||
buttonText: constants.CREATE,
|
||||
autoFilledName: '',
|
||||
callback: (collectionName) =>
|
||||
addToCollectionHelper(collectionName, null),
|
||||
});
|
||||
const moveToCollectionHelper = async (
|
||||
collectionName: string,
|
||||
collection: Collection
|
||||
) => {
|
||||
loadingBar.current?.continuousStart();
|
||||
try {
|
||||
await copyOrMoveFromCollection(
|
||||
COLLECTION_OPS_TYPE.MOVE,
|
||||
setCollectionSelectorView,
|
||||
selected,
|
||||
files,
|
||||
clearSelection,
|
||||
syncWithRemote,
|
||||
setActiveCollection,
|
||||
collectionName,
|
||||
collection
|
||||
);
|
||||
} catch (e) {
|
||||
setDialogMessage({
|
||||
title: constants.ERROR,
|
||||
staticBackdrop: true,
|
||||
close: { variant: 'danger' },
|
||||
content: constants.UNKNOWN_ERROR,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const showCreateCollectionModal = (opsType: COLLECTION_OPS_TYPE) => {
|
||||
try {
|
||||
let callback = null;
|
||||
switch (opsType) {
|
||||
case COLLECTION_OPS_TYPE.ADD:
|
||||
callback = (collectionName: string) =>
|
||||
addToCollectionHelper(collectionName, null);
|
||||
break;
|
||||
case COLLECTION_OPS_TYPE.MOVE:
|
||||
callback = (collectionName: string) =>
|
||||
moveToCollectionHelper(collectionName, null);
|
||||
break;
|
||||
default:
|
||||
throw Error(CustomError.INVALID_COLLECTION_OPERATION);
|
||||
}
|
||||
return () =>
|
||||
setCollectionNamerAttributes({
|
||||
title: constants.CREATE_COLLECTION,
|
||||
buttonText: constants.CREATE,
|
||||
autoFilledName: '',
|
||||
callback,
|
||||
});
|
||||
} catch (e) {
|
||||
logError(
|
||||
e,
|
||||
'showCreateCollectionModal called with incorrect attributes'
|
||||
);
|
||||
setDialogMessage({
|
||||
title: constants.ERROR,
|
||||
staticBackdrop: true,
|
||||
close: { variant: 'danger' },
|
||||
content: constants.UNKNOWN_ERROR,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const deleteFileHelper = async () => {
|
||||
loadingBar.current?.continuousStart();
|
||||
|
@ -368,11 +427,7 @@ export default function Gallery() {
|
|||
<GalleryContext.Provider value={defaultGalleryContext}>
|
||||
<FullScreenDropZone
|
||||
getRootProps={getRootProps}
|
||||
getInputProps={getInputProps}
|
||||
showCollectionSelector={setCollectionSelectorView.bind(
|
||||
null,
|
||||
true
|
||||
)}>
|
||||
getInputProps={getInputProps}>
|
||||
{loading && (
|
||||
<LoadingOverlay>
|
||||
<EnteSpinner />
|
||||
|
@ -488,6 +543,7 @@ export default function Gallery() {
|
|||
selected.collectionID === activeCollection && (
|
||||
<SelectedFileOptions
|
||||
addToCollectionHelper={addToCollectionHelper}
|
||||
moveToCollectionHelper={moveToCollectionHelper}
|
||||
showCreateCollectionModal={
|
||||
showCreateCollectionModal
|
||||
}
|
||||
|
@ -498,6 +554,7 @@ export default function Gallery() {
|
|||
deleteFileHelper={deleteFileHelper}
|
||||
count={selected.count}
|
||||
clearSelection={clearSelection}
|
||||
activeCollection={activeCollection}
|
||||
/>
|
||||
)}
|
||||
</FullScreenDropZone>
|
||||
|
|
|
@ -41,6 +41,23 @@ export interface Collection {
|
|||
isDeleted: boolean;
|
||||
}
|
||||
|
||||
interface EncryptedFileKey {
|
||||
id: number;
|
||||
encryptedKey: string;
|
||||
keyDecryptionNonce: string;
|
||||
}
|
||||
|
||||
interface AddToCollectionRequest {
|
||||
collectionID: number;
|
||||
files: EncryptedFileKey[];
|
||||
}
|
||||
|
||||
interface MoveToCollectionRequest {
|
||||
fromCollectionID: number;
|
||||
toCollectionID: number;
|
||||
files: EncryptedFileKey[];
|
||||
}
|
||||
|
||||
interface collectionAttributes {
|
||||
encryptedPath?: string;
|
||||
pathDecryptionNonce?: string;
|
||||
|
@ -344,39 +361,78 @@ export const addToCollection = async (
|
|||
files: File[]
|
||||
) => {
|
||||
try {
|
||||
const params = {};
|
||||
const worker = await new CryptoWorker();
|
||||
const token = getToken();
|
||||
params['collectionID'] = collection.id;
|
||||
await Promise.all(
|
||||
files.map(async (file) => {
|
||||
file.collectionID = collection.id;
|
||||
const newEncryptedKey: B64EncryptionResult =
|
||||
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;
|
||||
})
|
||||
);
|
||||
const fileKeysEncryptedWithNewCollection =
|
||||
await encryptWithNewCollectionKey(collection, files);
|
||||
|
||||
const requestBody: AddToCollectionRequest = {
|
||||
collectionID: collection.id,
|
||||
files: fileKeysEncryptedWithNewCollection,
|
||||
};
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/collections/add-files`,
|
||||
params,
|
||||
requestBody,
|
||||
null,
|
||||
{ 'X-Auth-Token': token }
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, 'Add to collection Failed ');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
export const moveToCollection = async (
|
||||
fromCollectionID: number,
|
||||
toCollection: Collection,
|
||||
files: File[]
|
||||
) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
const fileKeysEncryptedWithNewCollection =
|
||||
await encryptWithNewCollectionKey(toCollection, files);
|
||||
|
||||
const requestBody: MoveToCollectionRequest = {
|
||||
fromCollectionID: fromCollectionID,
|
||||
toCollectionID: toCollection.id,
|
||||
files: fileKeysEncryptedWithNewCollection,
|
||||
};
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/collections/move-files`,
|
||||
requestBody,
|
||||
null,
|
||||
{
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, 'move to collection Failed ');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const encryptWithNewCollectionKey = async (
|
||||
newCollection: Collection,
|
||||
files: File[]
|
||||
): Promise<EncryptedFileKey[]> => {
|
||||
const fileKeysEncryptedWithNewCollection: EncryptedFileKey[] = [];
|
||||
const worker = await new CryptoWorker();
|
||||
for (const file of files) {
|
||||
const newEncryptedKey: B64EncryptionResult = await worker.encryptToB64(
|
||||
file.key,
|
||||
newCollection.key
|
||||
);
|
||||
file.encryptedKey = newEncryptedKey.encryptedData;
|
||||
file.keyDecryptionNonce = newEncryptedKey.nonce;
|
||||
|
||||
fileKeysEncryptedWithNewCollection.push({
|
||||
id: file.id,
|
||||
encryptedKey: file.encryptedKey,
|
||||
keyDecryptionNonce: file.keyDecryptionNonce,
|
||||
});
|
||||
}
|
||||
return fileKeysEncryptedWithNewCollection;
|
||||
};
|
||||
const removeFromCollection = async (collection: Collection, files: File[]) => {
|
||||
try {
|
||||
const params = {};
|
||||
|
|
|
@ -3,13 +3,21 @@ import {
|
|||
Collection,
|
||||
CollectionType,
|
||||
createCollection,
|
||||
moveToCollection,
|
||||
} from 'services/collectionService';
|
||||
import { getSelectedFiles } from 'utils/file';
|
||||
import { File } from 'services/fileService';
|
||||
import { CustomError } from 'utils/common/errorUtil';
|
||||
import { SelectedState } from 'pages/gallery';
|
||||
|
||||
export async function addFilesToCollection(
|
||||
export enum COLLECTION_OPS_TYPE {
|
||||
ADD,
|
||||
MOVE,
|
||||
}
|
||||
export async function copyOrMoveFromCollection(
|
||||
type: COLLECTION_OPS_TYPE,
|
||||
setCollectionSelectorView: (value: boolean) => void,
|
||||
selected: any,
|
||||
selected: SelectedState,
|
||||
files: File[],
|
||||
clearSelection: () => void,
|
||||
syncWithRemote: () => Promise<void>,
|
||||
|
@ -28,7 +36,20 @@ export async function addFilesToCollection(
|
|||
collection = existingCollection;
|
||||
}
|
||||
const selectedFiles = getSelectedFiles(selected, files);
|
||||
await addToCollection(collection, selectedFiles);
|
||||
switch (type) {
|
||||
case COLLECTION_OPS_TYPE.ADD:
|
||||
await addToCollection(collection, selectedFiles);
|
||||
break;
|
||||
case COLLECTION_OPS_TYPE.MOVE:
|
||||
await moveToCollection(
|
||||
selected.collectionID,
|
||||
collection,
|
||||
selectedFiles
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw Error(CustomError.INVALID_COLLECTION_OPERATION);
|
||||
}
|
||||
clearSelection();
|
||||
await syncWithRemote();
|
||||
setActiveCollection(collection.id);
|
||||
|
|
|
@ -26,6 +26,7 @@ export enum CustomError {
|
|||
TYPE_DETECTION_FAILED = 'type detection failed',
|
||||
SIGNUP_FAILED = 'signup failed',
|
||||
FAV_COLLECTION_MISSING = 'favorite collection missing',
|
||||
INVALID_COLLECTION_OPERATION = 'invalid collection operation',
|
||||
}
|
||||
|
||||
function parseUploadError(error: AxiosResponse) {
|
||||
|
|
|
@ -542,6 +542,7 @@ const englishConstants = {
|
|||
TOO_LARGE_INFO:
|
||||
'these files were not uploaded as they exceed the maximum size limit for your storage plan',
|
||||
UPLOAD_TO_COLLECTION: 'upload to album',
|
||||
MOVE_TO_COLLECTION: 'move to collection',
|
||||
};
|
||||
|
||||
export default englishConstants;
|
||||
|
|
Loading…
Reference in a new issue