diff --git a/src/components/FullScreenDropZone.tsx b/src/components/FullScreenDropZone.tsx index d51ad0fcf..41ad96fb1 100644 --- a/src/components/FullScreenDropZone.tsx +++ b/src/components/FullScreenDropZone.tsx @@ -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 ( - {isDragActive && ( diff --git a/src/components/Sidebar/HelpSection.tsx b/src/components/Sidebar/HelpSection.tsx index 7909f0a25..308cad3eb 100644 --- a/src/components/Sidebar/HelpSection.tsx +++ b/src/components/Sidebar/HelpSection.tsx @@ -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()); } } diff --git a/src/components/pages/gallery/UploadButton.tsx b/src/components/Upload/UploadButton.tsx similarity index 100% rename from src/components/pages/gallery/UploadButton.tsx rename to src/components/Upload/UploadButton.tsx diff --git a/src/components/UploadProgress/dialog.tsx b/src/components/Upload/UploadProgress/dialog.tsx similarity index 100% rename from src/components/UploadProgress/dialog.tsx rename to src/components/Upload/UploadProgress/dialog.tsx diff --git a/src/components/UploadProgress/footer.tsx b/src/components/Upload/UploadProgress/footer.tsx similarity index 100% rename from src/components/UploadProgress/footer.tsx rename to src/components/Upload/UploadProgress/footer.tsx diff --git a/src/components/UploadProgress/header.tsx b/src/components/Upload/UploadProgress/header.tsx similarity index 100% rename from src/components/UploadProgress/header.tsx rename to src/components/Upload/UploadProgress/header.tsx diff --git a/src/components/UploadProgress/inProgressSection.tsx b/src/components/Upload/UploadProgress/inProgressSection.tsx similarity index 100% rename from src/components/UploadProgress/inProgressSection.tsx rename to src/components/Upload/UploadProgress/inProgressSection.tsx diff --git a/src/components/UploadProgress/index.tsx b/src/components/Upload/UploadProgress/index.tsx similarity index 100% rename from src/components/UploadProgress/index.tsx rename to src/components/Upload/UploadProgress/index.tsx diff --git a/src/components/UploadProgress/minimized.tsx b/src/components/Upload/UploadProgress/minimized.tsx similarity index 100% rename from src/components/UploadProgress/minimized.tsx rename to src/components/Upload/UploadProgress/minimized.tsx diff --git a/src/components/UploadProgress/progressBar.tsx b/src/components/Upload/UploadProgress/progressBar.tsx similarity index 100% rename from src/components/UploadProgress/progressBar.tsx rename to src/components/Upload/UploadProgress/progressBar.tsx diff --git a/src/components/UploadProgress/resultSection.tsx b/src/components/Upload/UploadProgress/resultSection.tsx similarity index 100% rename from src/components/UploadProgress/resultSection.tsx rename to src/components/Upload/UploadProgress/resultSection.tsx diff --git a/src/components/UploadProgress/section.tsx b/src/components/Upload/UploadProgress/section.tsx similarity index 100% rename from src/components/UploadProgress/section.tsx rename to src/components/Upload/UploadProgress/section.tsx diff --git a/src/components/UploadProgress/styledComponents.tsx b/src/components/Upload/UploadProgress/styledComponents.tsx similarity index 100% rename from src/components/UploadProgress/styledComponents.tsx rename to src/components/Upload/UploadProgress/styledComponents.tsx diff --git a/src/components/UploadProgress/title.tsx b/src/components/Upload/UploadProgress/title.tsx similarity index 100% rename from src/components/UploadProgress/title.tsx rename to src/components/Upload/UploadProgress/title.tsx diff --git a/src/components/pages/gallery/UploadStrategyChoiceModal.tsx b/src/components/Upload/UploadStrategyChoiceModal.tsx similarity index 100% rename from src/components/pages/gallery/UploadStrategyChoiceModal.tsx rename to src/components/Upload/UploadStrategyChoiceModal.tsx diff --git a/src/components/UploadTypeSelector/index.tsx b/src/components/Upload/UploadTypeSelector/index.tsx similarity index 100% rename from src/components/UploadTypeSelector/index.tsx rename to src/components/Upload/UploadTypeSelector/index.tsx diff --git a/src/components/UploadTypeSelector/option.tsx b/src/components/Upload/UploadTypeSelector/option.tsx similarity index 100% rename from src/components/UploadTypeSelector/option.tsx rename to src/components/Upload/UploadTypeSelector/option.tsx diff --git a/src/components/pages/gallery/Upload.tsx b/src/components/Upload/Uploader.tsx similarity index 88% rename from src/components/pages/gallery/Upload.tsx rename to src/components/Upload/Uploader.tsx index 77bc0e600..3b206f94d 100644 --- a/src/components/pages/gallery/Upload.tsx +++ b/src/components/Upload/Uploader.tsx @@ -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; - 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(); const [uploadFileNames, setUploadFileNames] = useState(); @@ -103,7 +105,7 @@ export default function Upload(props: Props) { const toUploadFiles = useRef(null); const isPendingDesktopUpload = useRef(false); const pendingDesktopUploadCollectionName = useRef(''); - const desktopUploadType = useRef(null); + const uploadType = useRef(null); const zipPaths = useRef(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 ( <> 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} /> + + + + + ); +} diff --git a/src/components/pages/gallery/Navbar.tsx b/src/components/pages/gallery/Navbar.tsx index 9ffb2f9c7..1e83db503 100644 --- a/src/components/pages/gallery/Navbar.tsx +++ b/src/components/pages/gallery/Navbar.tsx @@ -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; diff --git a/src/hooks/useFileInput.tsx b/src/hooks/useFileInput.tsx new file mode 100644 index 000000000..95fed9797 --- /dev/null +++ b/src/hooks/useFileInput.tsx @@ -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([]); + const inputRef = useRef(); + + const openSelectorDialog = useCallback(() => { + if (inputRef.current) { + inputRef.current.value = null; + inputRef.current.click(); + } + }, []); + + const handleChange: React.ChangeEventHandler = 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 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; +} diff --git a/src/pages/gallery/index.tsx b/src/pages/gallery/index.tsx index 90f66a575..b65b39d7b 100644 --- a/src/pages/gallery/index.tsx +++ b/src/pages/gallery/index.tsx @@ -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(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(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(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 ( + getDragAndDropRootProps={getDragAndDropRootProps}> + {blockingLoad && ( @@ -673,10 +692,8 @@ export default function Gallery() { setPhotoListHeader={setPhotoListHeader} /> - 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} /> { + return { + title: constants.DOWNLOAD_APP, + content: constants.DOWNLOAD_APP_MESSAGE, + + proceed: { + text: constants.DOWNLOAD, + action: downloadApp, + variant: 'accent', + }, + close: { + text: constants.CLOSE, + }, + }; +};