Merge pull request #622 from ente-io/upload-choice-screen

Upload choice screen for web
This commit is contained in:
Abhinav Kumar 2022-06-28 13:02:26 +05:30 committed by GitHub
commit 1f3126f5c0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 212 additions and 91 deletions

View file

@ -37,8 +37,7 @@ const Overlay = styled('div')`
`;
type Props = React.PropsWithChildren<{
getRootProps: any;
getInputProps: any;
getDragAndDropRootProps: any;
}>;
export default function FullScreenDropZone(props: Props) {
@ -55,10 +54,9 @@ export default function FullScreenDropZone(props: Props) {
}, []);
return (
<DropDiv
{...props.getRootProps({
{...props.getDragAndDropRootProps({
onDragEnter,
})}>
<input {...props.getInputProps()} />
{isDragActive && (
<Overlay onDrop={onDragLeave} onDragLeave={onDragLeave}>
<CloseButtonWrapper onClick={onDragLeave}>

View file

@ -6,9 +6,10 @@ import exportService from 'services/exportService';
import { getEndpoint } from 'utils/common/apiUtil';
import { getToken } from 'utils/common/key';
import isElectron from 'is-electron';
import { downloadApp, initiateEmail } from 'utils/common';
import { initiateEmail } from 'utils/common';
import { AppContext } from 'pages/_app';
import EnteSpinner from 'components/EnteSpinner';
import { getDownloadAppMessage } from 'utils/ui';
export default function HelpSection() {
const [exportModalView, setExportModalView] = useState(true);
@ -29,19 +30,7 @@ export default function HelpSection() {
if (isElectron()) {
setExportModalView(true);
} else {
setDialogMessage({
title: constants.DOWNLOAD_APP,
content: constants.DOWNLOAD_APP_MESSAGE,
proceed: {
text: constants.DOWNLOAD,
action: downloadApp,
variant: 'accent',
},
close: {
text: constants.CLOSE,
},
});
setDialogMessage(getDownloadAppMessage());
}
}

View file

@ -2,15 +2,14 @@ import React, { useContext, useEffect, useRef, useState } from 'react';
import { syncCollections, createAlbum } from 'services/collectionService';
import constants from 'utils/strings/constants';
import UploadProgress from '../../UploadProgress';
import UploadProgress from './UploadProgress';
import UploadStrategyChoiceModal from './UploadStrategyChoiceModal';
import { SetCollectionNamerAttributes } from '../../Collections/CollectionNamer';
import { SetCollectionNamerAttributes } from '../Collections/CollectionNamer';
import { SetCollectionSelectorAttributes } from 'types/gallery';
import { GalleryContext } from 'pages/gallery';
import { AppContext } from 'pages/_app';
import { logError } from 'utils/sentry';
import { FileRejection } from 'react-dropzone';
import UploadManager from 'services/upload/uploadManager';
import uploadManager from 'services/upload/uploadManager';
import ImportService from 'services/importService';
@ -20,7 +19,6 @@ import { CustomError } from 'utils/error';
import { Collection } from 'types/collection';
import { SetLoading, SetFiles } from 'types/gallery';
import { ElectronFile, FileWithCollection } from 'types/upload';
import UploadTypeSelector from '../../UploadTypeSelector';
import Router from 'next/router';
import { isCanvasBlocked } from 'utils/upload/isCanvasBlocked';
import { downloadApp } from 'utils/common';
@ -33,13 +31,14 @@ import {
InProgressUpload,
} from 'types/upload/ui';
import { UPLOAD_STAGES } from 'constants/upload';
import importService from 'services/importService';
import { getDownloadAppMessage } from 'utils/ui';
import UploadTypeSelector from './UploadTypeSelector';
const FIRST_ALBUM_NAME = 'My First Album';
interface Props {
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
droppedFiles: File[];
clearDroppedFiles: () => void;
closeCollectionSelector: () => void;
setCollectionSelectorAttributes: SetCollectionSelectorAttributes;
setCollectionNamerAttributes: SetCollectionNamerAttributes;
@ -47,14 +46,17 @@ interface Props {
uploadInProgress: boolean;
setUploadInProgress: (value: boolean) => void;
showCollectionSelector: () => void;
fileRejections: FileRejection[];
setFiles: SetFiles;
isFirstUpload: boolean;
electronFiles: ElectronFile[];
setElectronFiles: (files: ElectronFile[]) => void;
webFiles: File[];
setWebFiles: (files: File[]) => void;
uploadTypeSelectorView: boolean;
setUploadTypeSelectorView: (open: boolean) => void;
showSessionExpiredMessage: () => void;
showUploadFilesDialog: () => void;
showUploadDirsDialog: () => void;
}
enum UPLOAD_STRATEGY {
@ -62,7 +64,7 @@ enum UPLOAD_STRATEGY {
COLLECTION_PER_FOLDER,
}
export enum DESKTOP_UPLOAD_TYPE {
export enum UPLOAD_TYPE {
FILES = 'files',
FOLDERS = 'folders',
ZIPS = 'zips',
@ -78,7 +80,7 @@ const NULL_ANALYSIS_RESULT = {
multipleFolders: false,
};
export default function Upload(props: Props) {
export default function Uploader(props: Props) {
const [uploadProgressView, setUploadProgressView] = useState(false);
const [uploadStage, setUploadStage] = useState<UPLOAD_STAGES>();
const [uploadFileNames, setUploadFileNames] = useState<UploadFileNames>();
@ -103,7 +105,7 @@ export default function Upload(props: Props) {
const toUploadFiles = useRef<File[] | ElectronFile[]>(null);
const isPendingDesktopUpload = useRef(false);
const pendingDesktopUploadCollectionName = useRef<string>('');
const desktopUploadType = useRef<DESKTOP_UPLOAD_TYPE>(null);
const uploadType = useRef<UPLOAD_TYPE>(null);
const zipPaths = useRef<string[]>(null);
useEffect(() => {
@ -132,7 +134,7 @@ export default function Upload(props: Props) {
useEffect(() => {
if (
props.electronFiles?.length > 0 ||
props.droppedFiles?.length > 0 ||
props.webFiles?.length > 0 ||
appContext.sharedFiles?.length > 0
) {
if (props.uploadInProgress) {
@ -152,10 +154,10 @@ export default function Upload(props: Props) {
});
} else {
props.setLoading(true);
if (props.droppedFiles?.length > 0) {
if (props.webFiles?.length > 0) {
// File selection by drag and drop or selection of file.
toUploadFiles.current = props.droppedFiles;
props.clearDroppedFiles();
toUploadFiles.current = props.webFiles;
props.setWebFiles([]);
} else if (appContext.sharedFiles?.length > 0) {
toUploadFiles.current = appContext.sharedFiles;
appContext.resetSharedFiles();
@ -174,7 +176,7 @@ export default function Upload(props: Props) {
props.setLoading(false);
}
}
}, [props.droppedFiles, appContext.sharedFiles, props.electronFiles]);
}, [props.webFiles, appContext.sharedFiles, props.electronFiles]);
const uploadInit = function () {
setUploadStage(UPLOAD_STAGES.START);
@ -187,24 +189,20 @@ export default function Upload(props: Props) {
};
const resumeDesktopUpload = async (
type: DESKTOP_UPLOAD_TYPE,
type: UPLOAD_TYPE,
electronFiles: ElectronFile[],
collectionName: string
) => {
if (electronFiles && electronFiles?.length > 0) {
isPendingDesktopUpload.current = true;
pendingDesktopUploadCollectionName.current = collectionName;
desktopUploadType.current = type;
uploadType.current = type;
props.setElectronFiles(electronFiles);
}
};
function analyseUploadFiles(): AnalysisResult {
if (
isElectron() &&
(!desktopUploadType.current ||
desktopUploadType.current === DESKTOP_UPLOAD_TYPE.FILES)
) {
if (isElectron() && uploadType.current === UPLOAD_TYPE.FILES) {
return NULL_ANALYSIS_RESULT;
}
@ -343,13 +341,13 @@ export default function Upload(props: Props) {
await ImportService.setToUploadCollection(collections);
if (zipPaths.current) {
await ImportService.setToUploadFiles(
DESKTOP_UPLOAD_TYPE.ZIPS,
UPLOAD_TYPE.ZIPS,
zipPaths.current
);
zipPaths.current = null;
}
await ImportService.setToUploadFiles(
DESKTOP_UPLOAD_TYPE.FILES,
UPLOAD_TYPE.FILES,
filesWithCollectionToUpload.map(
({ file }) => (file as ElectronFile).path
)
@ -457,10 +455,7 @@ export default function Upload(props: Props) {
}
return;
}
if (
isElectron() &&
desktopUploadType.current === DESKTOP_UPLOAD_TYPE.ZIPS
) {
if (isElectron() && uploadType.current === UPLOAD_TYPE.ZIPS) {
uploadFilesToNewCollections(UPLOAD_STRATEGY.COLLECTION_PER_FOLDER);
return;
}
@ -482,12 +477,12 @@ export default function Upload(props: Props) {
title: constants.UPLOAD_TO_COLLECTION,
});
};
const handleDesktopUploadTypes = async (type: DESKTOP_UPLOAD_TYPE) => {
const handleDesktopUpload = async (type: UPLOAD_TYPE) => {
let files: ElectronFile[];
desktopUploadType.current = type;
if (type === DESKTOP_UPLOAD_TYPE.FILES) {
uploadType.current = type;
if (type === UPLOAD_TYPE.FILES) {
files = await ImportService.showUploadFilesDialog();
} else if (type === DESKTOP_UPLOAD_TYPE.FOLDERS) {
} else if (type === UPLOAD_TYPE.FOLDERS) {
files = await ImportService.showUploadDirsDialog();
} else {
const response = await ImportService.showUploadZipDialog();
@ -500,17 +495,42 @@ export default function Upload(props: Props) {
}
};
const handleWebUpload = async (type: UPLOAD_TYPE) => {
uploadType.current = type;
if (type === UPLOAD_TYPE.FILES) {
props.showUploadFilesDialog();
} else if (type === UPLOAD_TYPE.FOLDERS) {
props.showUploadDirsDialog();
} else {
appContext.setDialogMessage(getDownloadAppMessage());
}
};
const cancelUploads = async () => {
setUploadProgressView(false);
if (isElectron()) {
ImportService.cancelRemainingUploads();
}
await props.setUploadInProgress(false);
props.setUploadInProgress(false);
Router.reload();
};
const closeUploadProgress = () => setUploadProgressView(false);
const handleUpload = (type) => () => {
if (isElectron() && importService.checkAllElectronAPIsExists()) {
handleDesktopUpload(type);
} else {
handleWebUpload(type);
}
};
const handleFileUpload = handleUpload(UPLOAD_TYPE.FILES);
const handleFolderUpload = handleUpload(UPLOAD_TYPE.FOLDERS);
const handleZipUpload = handleUpload(UPLOAD_TYPE.ZIPS);
const closeUploadTypeSelector = () =>
props.setUploadTypeSelectorView(false);
return (
<>
<UploadStrategyChoiceModal
@ -529,16 +549,10 @@ export default function Upload(props: Props) {
/>
<UploadTypeSelector
show={props.uploadTypeSelectorView}
onHide={() => props.setUploadTypeSelectorView(false)}
uploadFiles={() =>
handleDesktopUploadTypes(DESKTOP_UPLOAD_TYPE.FILES)
}
uploadFolders={() =>
handleDesktopUploadTypes(DESKTOP_UPLOAD_TYPE.FOLDERS)
}
uploadGoogleTakeoutZips={() =>
handleDesktopUploadTypes(DESKTOP_UPLOAD_TYPE.ZIPS)
}
onHide={closeUploadTypeSelector}
uploadFiles={handleFileUpload}
uploadFolders={handleFolderUpload}
uploadGoogleTakeoutZips={handleZipUpload}
/>
<UploadProgress
open={uploadProgressView}

View file

@ -0,0 +1,15 @@
import React from 'react';
export default function UploadSelectorInputs({
getDragAndDropInputProps,
getFileSelectorInputProps,
getFolderSelectorInputProps,
}) {
return (
<>
<input {...getDragAndDropInputProps()} />
<input {...getFileSelectorInputProps()} />
<input {...getFolderSelectorInputProps()} />
</>
);
}

View file

@ -1,7 +1,6 @@
import React from 'react';
import NavbarBase from 'components/Navbar/base';
import SidebarToggler from 'components/Navbar/SidebarToggler';
import UploadButton from './UploadButton';
import { getNonTrashedUniqueUserFiles } from 'utils/file';
import SearchBar from 'components/Search/SearchBar';
import { FluidContainer } from 'components/Container';
@ -9,6 +8,7 @@ import { EnteLogo } from 'components/EnteLogo';
import { Collection } from 'types/collection';
import { EnteFile } from 'types/file';
import { UpdateSearch } from 'types/search';
import UploadButton from 'components/Upload/UploadButton';
interface Iprops {
openSidebar: () => void;

View file

@ -0,0 +1,66 @@
import { useCallback, useRef, useState } from 'react';
import { FileWithPath } from 'file-selector';
export default function useFileInput({ directory }: { directory?: boolean }) {
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const inputRef = useRef<HTMLInputElement>();
const openSelectorDialog = useCallback(() => {
if (inputRef.current) {
inputRef.current.value = null;
inputRef.current.click();
}
}, []);
const handleChange: React.ChangeEventHandler<HTMLInputElement> = async (
event
) => {
if (!!event.target && !!event.target.files) {
const files = [...event.target.files].map((file) =>
toFileWithPath(file)
);
setSelectedFiles(files);
}
};
const getInputProps = useCallback(
() => ({
type: 'file',
style: { display: 'none' },
...(directory ? { directory: '', webkitdirectory: '' } : {}),
ref: inputRef,
onChange: handleChange,
}),
[]
);
return {
getInputProps,
open: openSelectorDialog,
selectedFiles: selectedFiles,
};
}
// https://github.com/react-dropzone/file-selector/blob/master/src/file.ts#L88
export function toFileWithPath(file: File, path?: string): FileWithPath {
if (typeof (file as any).path !== 'string') {
// on electron, path is already set to the absolute path
const { webkitRelativePath } = file;
Object.defineProperty(file, 'path', {
value:
typeof path === 'string'
? path
: typeof webkitRelativePath === 'string' && // If <input webkitdirectory> is set,
// the File will have a {webkitRelativePath} property
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory
webkitRelativePath.length > 0
? webkitRelativePath
: file.name,
writable: false,
configurable: false,
enumerable: true,
});
}
return file;
}

View file

@ -57,7 +57,7 @@ import CollectionNamer, {
CollectionNamerAttributes,
} from 'components/Collections/CollectionNamer';
import PlanSelector from 'components/pages/gallery/PlanSelector';
import Upload from 'components/pages/gallery/Upload';
import Uploader from 'components/Upload/Uploader';
import {
ALL_SECTION,
ARCHIVE_SECTION,
@ -96,13 +96,14 @@ import { GalleryContextType, SelectedState } from 'types/gallery';
import { VISIBILITY_STATE } from 'types/magicMetadata';
import Notification from 'components/Notification';
import { ElectronFile } from 'types/upload';
import importService from 'services/importService';
import Collections from 'components/Collections';
import { GalleryNavbar } from 'components/pages/gallery/Navbar';
import { Search, SearchResultSummary, UpdateSearch } from 'types/search';
import SearchResultInfo from 'components/Search/SearchResultInfo';
import { NotificationAttributes } from 'types/Notification';
import { ITEM_TYPE, TimeStampListItem } from 'components/PhotoList';
import UploadInputs from 'components/UploadSelectorInputs';
import useFileInput from 'hooks/useFileInput';
export const DeadCenter = styled('div')`
flex: 1;
@ -158,16 +159,28 @@ export default function Gallery() {
const [search, setSearch] = useState<Search>(null);
const [uploadInProgress, setUploadInProgress] = useState(false);
const {
getRootProps,
getInputProps,
open: openFileUploader,
acceptedFiles,
fileRejections,
getRootProps: getDragAndDropRootProps,
getInputProps: getDragAndDropInputProps,
acceptedFiles: dragAndDropFiles,
} = useDropzone({
noClick: true,
noKeyboard: true,
disabled: uploadInProgress,
});
const {
selectedFiles: fileSelectorFiles,
open: openFileSelector,
getInputProps: getFileSelectorInputProps,
} = useFileInput({
directory: false,
});
const {
selectedFiles: folderSelectorFiles,
open: openFolderSelector,
getInputProps: getFolderSelectorInputProps,
} = useFileInput({
directory: true,
});
const [isInSearchMode, setIsInSearchMode] = useState(false);
const [searchResultSummary, setSetSearchResultSummary] =
@ -198,13 +211,13 @@ export default function Gallery() {
const showPlanSelectorModal = () => setPlanModalView(true);
const [electronFiles, setElectronFiles] = useState<ElectronFile[]>(null);
const [webFiles, setWebFiles] = useState([]);
const [uploadTypeSelectorView, setUploadTypeSelectorView] = useState(false);
const [sidebarView, setSidebarView] = useState(true);
const closeSidebar = () => setSidebarView(false);
const openSidebar = () => setSidebarView(true);
const [droppedFiles, setDroppedFiles] = useState([]);
const [photoListHeader, setPhotoListHeader] =
useState<TimeStampListItem>(null);
@ -275,7 +288,15 @@ export default function Gallery() {
[notificationAttributes]
);
useEffect(() => setDroppedFiles(acceptedFiles), [acceptedFiles]);
useEffect(() => {
if (dragAndDropFiles?.length > 0) {
setWebFiles(dragAndDropFiles);
} else if (folderSelectorFiles?.length > 0) {
setWebFiles(folderSelectorFiles);
} else if (fileSelectorFiles?.length > 0) {
setWebFiles(fileSelectorFiles);
}
}, [dragAndDropFiles, fileSelectorFiles, folderSelectorFiles]);
useEffect(() => {
if (typeof activeCollection === 'undefined') {
@ -587,19 +608,13 @@ export default function Gallery() {
finishLoading();
};
const openUploader = () => {
if (importService.checkAllElectronAPIsExists()) {
setUploadTypeSelectorView(true);
} else {
openFileUploader();
}
};
const resetSearch = () => {
setSearch(null);
setSetSearchResultSummary(null);
};
const openUploader = () => setUploadTypeSelectorView(true);
return (
<GalleryContext.Provider
value={{
@ -612,8 +627,12 @@ export default function Gallery() {
photoListHeader: photoListHeader,
}}>
<FullScreenDropZone
getRootProps={getRootProps}
getInputProps={getInputProps}>
getDragAndDropRootProps={getDragAndDropRootProps}>
<UploadInputs
getDragAndDropInputProps={getDragAndDropInputProps}
getFileSelectorInputProps={getFileSelectorInputProps}
getFolderSelectorInputProps={getFolderSelectorInputProps}
/>
{blockingLoad && (
<LoadingOverlay>
<EnteSpinner />
@ -673,10 +692,8 @@ export default function Gallery() {
setPhotoListHeader={setPhotoListHeader}
/>
<Upload
<Uploader
syncWithRemote={syncWithRemote}
droppedFiles={droppedFiles}
clearDroppedFiles={() => setDroppedFiles([])}
showCollectionSelector={setCollectionSelectorView.bind(
null,
true
@ -692,13 +709,16 @@ export default function Gallery() {
setCollectionNamerAttributes={setCollectionNamerAttributes}
uploadInProgress={uploadInProgress}
setUploadInProgress={setUploadInProgress}
fileRejections={fileRejections}
setFiles={setFiles}
isFirstUpload={hasNonEmptyCollections(collectionSummaries)}
electronFiles={electronFiles}
setElectronFiles={setElectronFiles}
webFiles={webFiles}
setWebFiles={setWebFiles}
uploadTypeSelectorView={uploadTypeSelectorView}
setUploadTypeSelectorView={setUploadTypeSelectorView}
showUploadFilesDialog={openFileSelector}
showUploadDirsDialog={openFolderSelector}
showSessionExpiredMessage={showSessionExpiredMessage}
/>
<Sidebar

View file

@ -1,4 +1,4 @@
import { DESKTOP_UPLOAD_TYPE } from 'components/pages/gallery/Upload';
import { UPLOAD_TYPE } from 'components/Upload/Uploader';
import { Collection } from 'types/collection';
import { ElectronFile, FileWithCollection } from 'types/upload';
import { runningInBrowser } from 'utils/common';
@ -7,7 +7,7 @@ import { logError } from 'utils/sentry';
interface PendingUploads {
files: ElectronFile[];
collectionName: string;
type: DESKTOP_UPLOAD_TYPE;
type: UPLOAD_TYPE;
}
interface selectZipResult {
@ -83,7 +83,7 @@ class ImportService {
}
async setToUploadFiles(
type: DESKTOP_UPLOAD_TYPE.FILES | DESKTOP_UPLOAD_TYPE.ZIPS,
type: UPLOAD_TYPE.FILES | UPLOAD_TYPE.ZIPS,
filePaths: string[]
) {
if (this.allElectronAPIsExist) {
@ -112,14 +112,14 @@ class ImportService {
);
}
}
this.setToUploadFiles(DESKTOP_UPLOAD_TYPE.FILES, filePaths);
this.setToUploadFiles(UPLOAD_TYPE.FILES, filePaths);
}
}
cancelRemainingUploads() {
if (this.allElectronAPIsExist) {
this.ElectronAPIs.setToUploadCollection(null);
this.ElectronAPIs.setToUploadFiles(DESKTOP_UPLOAD_TYPE.ZIPS, []);
this.ElectronAPIs.setToUploadFiles(DESKTOP_UPLOAD_TYPE.FILES, []);
this.ElectronAPIs.setToUploadFiles(UPLOAD_TYPE.ZIPS, []);
this.ElectronAPIs.setToUploadFiles(UPLOAD_TYPE.FILES, []);
}
}
}

19
src/utils/ui/index.tsx Normal file
View file

@ -0,0 +1,19 @@
import { DialogBoxAttributes } from 'types/dialogBox';
import { downloadApp } from 'utils/common';
import constants from 'utils/strings/constants';
export const getDownloadAppMessage = (): DialogBoxAttributes => {
return {
title: constants.DOWNLOAD_APP,
content: constants.DOWNLOAD_APP_MESSAGE,
proceed: {
text: constants.DOWNLOAD,
action: downloadApp,
variant: 'accent',
},
close: {
text: constants.CLOSE,
},
};
};