[web] Deduce parent directory (#1663)
This commit is contained in:
commit
70878e8f54
9 changed files with 272 additions and 198 deletions
|
@ -37,7 +37,7 @@
|
|||
"photoswipe": "file:./thirdparty/photoswipe",
|
||||
"piexifjs": "^1.0.6",
|
||||
"pure-react-carousel": "^1.30.1",
|
||||
"react-dropzone": "^11.2.4",
|
||||
"react-dropzone": "^14.2",
|
||||
"react-otp-input": "^2.3.1",
|
||||
"react-select": "^4.3.1",
|
||||
"react-top-loading-bar": "^2.0.1",
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { basename } from "@/next/file";
|
||||
import log from "@/next/log";
|
||||
import type { CollectionMapping, Electron, ZipItem } from "@/next/types/ipc";
|
||||
import { firstNonEmpty } from "@/utils/array";
|
||||
import { ensure } from "@/utils/ensure";
|
||||
import { CustomError } from "@ente/shared/error";
|
||||
import { isPromise } from "@ente/shared/utils";
|
||||
import DiscFullIcon from "@mui/icons-material/DiscFull";
|
||||
|
@ -324,17 +326,17 @@ export default function Uploader({
|
|||
|
||||
// Trigger an upload when any of the dependencies change.
|
||||
useEffect(() => {
|
||||
// Re the paths:
|
||||
// About the paths:
|
||||
//
|
||||
// - These are not necessarily the full paths. In particular, when
|
||||
// running on the browser they'll be the relative paths (at best) or
|
||||
// just the file-name otherwise.
|
||||
//
|
||||
// - All the paths use POSIX separators. See inline comments.
|
||||
//
|
||||
const allItemAndPaths = [
|
||||
// See: [Note: webkitRelativePath]. In particular, they use POSIX
|
||||
// separators.
|
||||
webFiles.map((f) => [f, f.webkitRelativePath ?? f.name]),
|
||||
// Relative path (using POSIX separators) or the file's name.
|
||||
webFiles.map((f) => [f, pathLikeForWebFile(f)]),
|
||||
// The paths we get from the desktop app all eventually come either
|
||||
// from electron.selectDirectory or electron.pathForFile, both of
|
||||
// which return POSIX paths.
|
||||
|
@ -822,6 +824,37 @@ const desktopFilesAndZipItems = async (electron: Electron, files: File[]) => {
|
|||
return { fileAndPaths, zipItems };
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the relative path or name of a File object selected or
|
||||
* drag-and-dropped on the web.
|
||||
*
|
||||
* There are three cases here:
|
||||
*
|
||||
* 1. If the user selects individual file(s), then the returned File objects
|
||||
* will only have a `name`.
|
||||
*
|
||||
* 2. If the user selects directory(ies), then the returned File objects will
|
||||
* have a `webkitRelativePath`. For more details, see [Note:
|
||||
* webkitRelativePath]. In particular, these will POSIX separators.
|
||||
*
|
||||
* 3. If the user drags-and-drops, then the react-dropzone library that we use
|
||||
* will internally convert `webkitRelativePath` to `path`, but otherwise it
|
||||
* behaves same as case 2.
|
||||
* https://github.com/react-dropzone/file-selector/blob/master/src/file.ts#L1214
|
||||
*/
|
||||
const pathLikeForWebFile = (file: File): string =>
|
||||
ensure(
|
||||
firstNonEmpty([
|
||||
// We need to check first, since path is not a property of
|
||||
// the standard File objects.
|
||||
"path" in file && typeof file.path == "string"
|
||||
? file.path
|
||||
: undefined,
|
||||
file.webkitRelativePath,
|
||||
file.name,
|
||||
]),
|
||||
);
|
||||
|
||||
// This is used to prompt the user the make upload strategy choice
|
||||
interface ImportSuggestion {
|
||||
rootFolderName: string;
|
||||
|
|
|
@ -1,9 +1,24 @@
|
|||
export default function UploadSelectorInputs({
|
||||
type GetInputProps = () => React.HTMLAttributes<HTMLInputElement>;
|
||||
|
||||
interface UploadSelectorInputsProps {
|
||||
getDragAndDropInputProps: GetInputProps;
|
||||
getFileSelectorInputProps: GetInputProps;
|
||||
getFolderSelectorInputProps: GetInputProps;
|
||||
getZipFileSelectorInputProps?: GetInputProps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a bunch of HTML inputs elements, one each for the given props.
|
||||
*
|
||||
* These hidden input element serve as the way for us to show various file /
|
||||
* folder Selector dialogs and handle drag and drop inputs.
|
||||
*/
|
||||
export const UploadSelectorInputs: React.FC<UploadSelectorInputsProps> = ({
|
||||
getDragAndDropInputProps,
|
||||
getFileSelectorInputProps,
|
||||
getFolderSelectorInputProps,
|
||||
getZipFileSelectorInputProps,
|
||||
}) {
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<input {...getDragAndDropInputProps()} />
|
||||
|
@ -14,4 +29,4 @@ export default function UploadSelectorInputs({
|
|||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,82 +1,36 @@
|
|||
import {
|
||||
SESSION_KEYS,
|
||||
clearKeys,
|
||||
getKey,
|
||||
} from "@ente/shared/storage/sessionStorage";
|
||||
import { Typography, styled } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
constructEmailList,
|
||||
createAlbum,
|
||||
getAllLatestCollections,
|
||||
getAllLocalCollections,
|
||||
getCollectionSummaries,
|
||||
getFavItemIds,
|
||||
getHiddenItemsSummary,
|
||||
getSectionSummaries,
|
||||
} from "services/collectionService";
|
||||
import { getLocalFiles, syncFiles } from "services/fileService";
|
||||
|
||||
import { checkSubscriptionPurchase } from "utils/billing";
|
||||
|
||||
import log from "@/next/log";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { CenteredFlex } from "@ente/shared/components/Container";
|
||||
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
||||
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
|
||||
import { CustomError } from "@ente/shared/error";
|
||||
import { useFileInput } from "@ente/shared/hooks/useFileInput";
|
||||
import useMemoSingleThreaded from "@ente/shared/hooks/useMemoSingleThreaded";
|
||||
import InMemoryStore, { MS_KEYS } from "@ente/shared/storage/InMemoryStore";
|
||||
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
|
||||
import {
|
||||
getToken,
|
||||
isFirstLogin,
|
||||
justSignedUp,
|
||||
setIsFirstLogin,
|
||||
setJustSignedUp,
|
||||
} from "@ente/shared/storage/localStorage/helpers";
|
||||
import CollectionSelector, {
|
||||
CollectionSelectorAttributes,
|
||||
} from "components/Collections/CollectionSelector";
|
||||
import FullScreenDropZone from "components/FullScreenDropZone";
|
||||
import { LoadingOverlay } from "components/LoadingOverlay";
|
||||
import PhotoFrame from "components/PhotoFrame";
|
||||
import Sidebar from "components/Sidebar";
|
||||
import SelectedFileOptions from "components/pages/gallery/SelectedFileOptions";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import {
|
||||
isTokenValid,
|
||||
syncMapEnabled,
|
||||
validateKey,
|
||||
} from "services/userService";
|
||||
import { preloadImage } from "utils/common";
|
||||
import {
|
||||
FILE_OPS_TYPE,
|
||||
constructFileToCollectionMap,
|
||||
getSelectedFiles,
|
||||
getUniqueFiles,
|
||||
handleFileOps,
|
||||
mergeMetadata,
|
||||
sortFiles,
|
||||
} from "utils/file";
|
||||
|
||||
import log from "@/next/log";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import { CenteredFlex } from "@ente/shared/components/Container";
|
||||
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
|
||||
import { CustomError } from "@ente/shared/error";
|
||||
import useFileInput from "@ente/shared/hooks/useFileInput";
|
||||
import useMemoSingleThreaded from "@ente/shared/hooks/useMemoSingleThreaded";
|
||||
import InMemoryStore, { MS_KEYS } from "@ente/shared/storage/InMemoryStore";
|
||||
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
|
||||
import { getToken } from "@ente/shared/storage/localStorage/helpers";
|
||||
SESSION_KEYS,
|
||||
clearKeys,
|
||||
getKey,
|
||||
} from "@ente/shared/storage/sessionStorage";
|
||||
import { User } from "@ente/shared/user/types";
|
||||
import { isPromise } from "@ente/shared/utils";
|
||||
import { Typography, styled } from "@mui/material";
|
||||
import AuthenticateUserModal from "components/AuthenticateUserModal";
|
||||
import Collections from "components/Collections";
|
||||
import CollectionNamer, {
|
||||
CollectionNamerAttributes,
|
||||
} from "components/Collections/CollectionNamer";
|
||||
import CollectionSelector, {
|
||||
CollectionSelectorAttributes,
|
||||
} from "components/Collections/CollectionSelector";
|
||||
import ExportModal from "components/ExportModal";
|
||||
import {
|
||||
FilesDownloadProgress,
|
||||
|
@ -85,13 +39,18 @@ import {
|
|||
import FixCreationTime, {
|
||||
FixCreationTimeAttributes,
|
||||
} from "components/FixCreationTime";
|
||||
import FullScreenDropZone from "components/FullScreenDropZone";
|
||||
import GalleryEmptyState from "components/GalleryEmptyState";
|
||||
import { LoadingOverlay } from "components/LoadingOverlay";
|
||||
import PhotoFrame from "components/PhotoFrame";
|
||||
import { ITEM_TYPE, TimeStampListItem } from "components/PhotoList";
|
||||
import SearchResultInfo from "components/Search/SearchResultInfo";
|
||||
import Sidebar from "components/Sidebar";
|
||||
import Uploader from "components/Upload/Uploader";
|
||||
import UploadInputs from "components/UploadSelectorInputs";
|
||||
import { UploadSelectorInputs } from "components/UploadSelectorInputs";
|
||||
import { GalleryNavbar } from "components/pages/gallery/Navbar";
|
||||
import PlanSelector from "components/pages/gallery/PlanSelector";
|
||||
import SelectedFileOptions from "components/pages/gallery/SelectedFileOptions";
|
||||
import {
|
||||
ALL_SECTION,
|
||||
ARCHIVE_SECTION,
|
||||
|
@ -100,15 +59,42 @@ import {
|
|||
TRASH_SECTION,
|
||||
} from "constants/collection";
|
||||
import { SYNC_INTERVAL_IN_MICROSECONDS } from "constants/gallery";
|
||||
import { t } from "i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import { AppContext } from "pages/_app";
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { clipService } from "services/clip-service";
|
||||
import { constructUserIDToEmailMap } from "services/collectionService";
|
||||
import {
|
||||
constructEmailList,
|
||||
constructUserIDToEmailMap,
|
||||
createAlbum,
|
||||
getAllLatestCollections,
|
||||
getAllLocalCollections,
|
||||
getCollectionSummaries,
|
||||
getFavItemIds,
|
||||
getHiddenItemsSummary,
|
||||
getSectionSummaries,
|
||||
} from "services/collectionService";
|
||||
import downloadManager from "services/download";
|
||||
import { syncEmbeddings, syncFileEmbeddings } from "services/embeddingService";
|
||||
import { syncEntities } from "services/entityService";
|
||||
import { getLocalFiles, syncFiles } from "services/fileService";
|
||||
import locationSearchService from "services/locationSearchService";
|
||||
import { getLocalTrashedFiles, syncTrash } from "services/trashService";
|
||||
import uploadManager from "services/upload/uploadManager";
|
||||
import {
|
||||
isTokenValid,
|
||||
syncMapEnabled,
|
||||
validateKey,
|
||||
} from "services/userService";
|
||||
import { Collection, CollectionSummaries } from "types/collection";
|
||||
import { EnteFile } from "types/file";
|
||||
import {
|
||||
|
@ -120,6 +106,7 @@ import {
|
|||
} from "types/gallery";
|
||||
import { Search, SearchResultSummary, UpdateSearch } from "types/search";
|
||||
import { FamilyData } from "types/user";
|
||||
import { checkSubscriptionPurchase } from "utils/billing";
|
||||
import {
|
||||
COLLECTION_OPS_TYPE,
|
||||
constructCollectionNameMap,
|
||||
|
@ -131,6 +118,16 @@ import {
|
|||
splitNormalAndHiddenCollections,
|
||||
} from "utils/collection";
|
||||
import ComlinkSearchWorker from "utils/comlink/ComlinkSearchWorker";
|
||||
import { preloadImage } from "utils/common";
|
||||
import {
|
||||
FILE_OPS_TYPE,
|
||||
constructFileToCollectionMap,
|
||||
getSelectedFiles,
|
||||
getUniqueFiles,
|
||||
handleFileOps,
|
||||
mergeMetadata,
|
||||
sortFiles,
|
||||
} from "utils/file";
|
||||
import { isArchivedFile } from "utils/magicMetadata";
|
||||
import { getSessionExpiredMessage } from "utils/ui";
|
||||
import { getLocalFamilyData } from "utils/user/family";
|
||||
|
@ -201,8 +198,11 @@ export default function Gallery() {
|
|||
const [isPhotoSwipeOpen, setIsPhotoSwipeOpen] = useState(false);
|
||||
|
||||
const {
|
||||
// A function to call to get the props we should apply to the container,
|
||||
getRootProps: getDragAndDropRootProps,
|
||||
// ... the props we should apply to the <input> element,
|
||||
getInputProps: getDragAndDropInputProps,
|
||||
// ... and the files that we got.
|
||||
acceptedFiles: dragAndDropFiles,
|
||||
} = useDropzone({
|
||||
noClick: true,
|
||||
|
@ -210,23 +210,23 @@ export default function Gallery() {
|
|||
disabled: shouldDisableDropzone,
|
||||
});
|
||||
const {
|
||||
selectedFiles: fileSelectorFiles,
|
||||
open: openFileSelector,
|
||||
getInputProps: getFileSelectorInputProps,
|
||||
openSelector: openFileSelector,
|
||||
selectedFiles: fileSelectorFiles,
|
||||
} = useFileInput({
|
||||
directory: false,
|
||||
});
|
||||
const {
|
||||
selectedFiles: folderSelectorFiles,
|
||||
open: openFolderSelector,
|
||||
getInputProps: getFolderSelectorInputProps,
|
||||
openSelector: openFolderSelector,
|
||||
selectedFiles: folderSelectorFiles,
|
||||
} = useFileInput({
|
||||
directory: true,
|
||||
});
|
||||
const {
|
||||
selectedFiles: fileSelectorZipFiles,
|
||||
open: openZipFileSelector,
|
||||
getInputProps: getZipFileSelectorInputProps,
|
||||
openSelector: openZipFileSelector,
|
||||
selectedFiles: fileSelectorZipFiles,
|
||||
} = useFileInput({
|
||||
directory: false,
|
||||
accept: ".zip",
|
||||
|
@ -1013,14 +1013,14 @@ export default function Gallery() {
|
|||
setSelectedFiles: setSelected,
|
||||
}}
|
||||
>
|
||||
<FullScreenDropZone
|
||||
getDragAndDropRootProps={getDragAndDropRootProps}
|
||||
>
|
||||
<UploadInputs
|
||||
getDragAndDropInputProps={getDragAndDropInputProps}
|
||||
getFileSelectorInputProps={getFileSelectorInputProps}
|
||||
getFolderSelectorInputProps={getFolderSelectorInputProps}
|
||||
getZipFileSelectorInputProps={getZipFileSelectorInputProps}
|
||||
<FullScreenDropZone {...{ getDragAndDropRootProps }}>
|
||||
<UploadSelectorInputs
|
||||
{...{
|
||||
getDragAndDropInputProps,
|
||||
getFileSelectorInputProps,
|
||||
getFolderSelectorInputProps,
|
||||
getZipFileSelectorInputProps,
|
||||
}}
|
||||
/>
|
||||
{blockingLoad && (
|
||||
<LoadingOverlay>
|
||||
|
|
|
@ -1,15 +1,51 @@
|
|||
import log from "@/next/log";
|
||||
import { logoutUser } from "@ente/accounts/services/user";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import {
|
||||
CenteredFlex,
|
||||
SpaceBetweenFlex,
|
||||
VerticallyCentered,
|
||||
} from "@ente/shared/components/Container";
|
||||
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
||||
import FormPaper from "@ente/shared/components/Form/FormPaper";
|
||||
import FormPaperTitle from "@ente/shared/components/Form/FormPaper/Title";
|
||||
import OverflowMenu from "@ente/shared/components/OverflowMenu/menu";
|
||||
import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option";
|
||||
import SingleInputForm, {
|
||||
SingleInputFormProps,
|
||||
} from "@ente/shared/components/SingleInputForm";
|
||||
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
|
||||
import { ENTE_WEBSITE_LINK } from "@ente/shared/constants/urls";
|
||||
import ComlinkCryptoWorker from "@ente/shared/crypto";
|
||||
import { CustomError, parseSharingErrorCodes } from "@ente/shared/error";
|
||||
import { useFileInput } from "@ente/shared/hooks/useFileInput";
|
||||
import AddPhotoAlternateOutlined from "@mui/icons-material/AddPhotoAlternateOutlined";
|
||||
import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined";
|
||||
import MoreHoriz from "@mui/icons-material/MoreHoriz";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import bs58 from "bs58";
|
||||
import { CollectionInfo } from "components/Collections/CollectionInfo";
|
||||
import { CollectionInfoBarWrapper } from "components/Collections/styledComponents";
|
||||
import {
|
||||
FilesDownloadProgress,
|
||||
FilesDownloadProgressAttributes,
|
||||
} from "components/FilesDownloadProgress";
|
||||
import FullScreenDropZone from "components/FullScreenDropZone";
|
||||
import { LoadingOverlay } from "components/LoadingOverlay";
|
||||
import PhotoFrame from "components/PhotoFrame";
|
||||
import { ITEM_TYPE, TimeStampListItem } from "components/PhotoList";
|
||||
import UploadButton from "components/Upload/UploadButton";
|
||||
import Uploader from "components/Upload/Uploader";
|
||||
import { UploadSelectorInputs } from "components/UploadSelectorInputs";
|
||||
import SharedAlbumNavbar from "components/pages/sharedAlbum/Navbar";
|
||||
import SelectedFileOptions from "components/pages/sharedAlbum/SelectedFileOptions";
|
||||
import { ALL_SECTION } from "constants/collection";
|
||||
import { t } from "i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import downloadManager from "services/download";
|
||||
import {
|
||||
getLocalPublicCollection,
|
||||
getLocalPublicCollectionPassword,
|
||||
|
@ -25,50 +61,6 @@ import {
|
|||
} from "services/publicCollectionService";
|
||||
import { Collection } from "types/collection";
|
||||
import { EnteFile } from "types/file";
|
||||
import {
|
||||
downloadSelectedFiles,
|
||||
getSelectedFiles,
|
||||
mergeMetadata,
|
||||
sortFiles,
|
||||
} from "utils/file";
|
||||
import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
|
||||
|
||||
import { logoutUser } from "@ente/accounts/services/user";
|
||||
import { APPS } from "@ente/shared/apps/constants";
|
||||
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
||||
import FormPaper from "@ente/shared/components/Form/FormPaper";
|
||||
import FormPaperTitle from "@ente/shared/components/Form/FormPaper/Title";
|
||||
import OverflowMenu from "@ente/shared/components/OverflowMenu/menu";
|
||||
import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option";
|
||||
import SingleInputForm, {
|
||||
SingleInputFormProps,
|
||||
} from "@ente/shared/components/SingleInputForm";
|
||||
import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages";
|
||||
import { ENTE_WEBSITE_LINK } from "@ente/shared/constants/urls";
|
||||
import ComlinkCryptoWorker from "@ente/shared/crypto";
|
||||
import useFileInput from "@ente/shared/hooks/useFileInput";
|
||||
import AddPhotoAlternateOutlined from "@mui/icons-material/AddPhotoAlternateOutlined";
|
||||
import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined";
|
||||
import MoreHoriz from "@mui/icons-material/MoreHoriz";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import bs58 from "bs58";
|
||||
import { CollectionInfo } from "components/Collections/CollectionInfo";
|
||||
import { CollectionInfoBarWrapper } from "components/Collections/styledComponents";
|
||||
import {
|
||||
FilesDownloadProgress,
|
||||
FilesDownloadProgressAttributes,
|
||||
} from "components/FilesDownloadProgress";
|
||||
import FullScreenDropZone from "components/FullScreenDropZone";
|
||||
import { LoadingOverlay } from "components/LoadingOverlay";
|
||||
import { ITEM_TYPE, TimeStampListItem } from "components/PhotoList";
|
||||
import UploadButton from "components/Upload/UploadButton";
|
||||
import Uploader from "components/Upload/Uploader";
|
||||
import UploadSelectorInputs from "components/UploadSelectorInputs";
|
||||
import SharedAlbumNavbar from "components/pages/sharedAlbum/Navbar";
|
||||
import SelectedFileOptions from "components/pages/sharedAlbum/SelectedFileOptions";
|
||||
import { useRouter } from "next/router";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import downloadManager from "services/download";
|
||||
import {
|
||||
SelectedState,
|
||||
SetFilesDownloadProgressAttributes,
|
||||
|
@ -76,6 +68,13 @@ import {
|
|||
UploadTypeSelectorIntent,
|
||||
} from "types/gallery";
|
||||
import { downloadCollectionFiles, isHiddenCollection } from "utils/collection";
|
||||
import {
|
||||
downloadSelectedFiles,
|
||||
getSelectedFiles,
|
||||
mergeMetadata,
|
||||
sortFiles,
|
||||
} from "utils/file";
|
||||
import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
|
||||
|
||||
export default function PublicCollectionGallery() {
|
||||
const token = useRef<string>(null);
|
||||
|
@ -118,16 +117,16 @@ export default function PublicCollectionGallery() {
|
|||
disabled: shouldDisableDropzone,
|
||||
});
|
||||
const {
|
||||
selectedFiles: fileSelectorFiles,
|
||||
open: openFileSelector,
|
||||
getInputProps: getFileSelectorInputProps,
|
||||
openSelector: openFileSelector,
|
||||
selectedFiles: fileSelectorFiles,
|
||||
} = useFileInput({
|
||||
directory: false,
|
||||
});
|
||||
const {
|
||||
selectedFiles: folderSelectorFiles,
|
||||
open: openFolderSelector,
|
||||
getInputProps: getFolderSelectorInputProps,
|
||||
openSelector: openFolderSelector,
|
||||
selectedFiles: folderSelectorFiles,
|
||||
} = useFileInput({
|
||||
directory: true,
|
||||
});
|
||||
|
@ -543,14 +542,13 @@ export default function PublicCollectionGallery() {
|
|||
photoListFooter,
|
||||
}}
|
||||
>
|
||||
<FullScreenDropZone
|
||||
getDragAndDropRootProps={getDragAndDropRootProps}
|
||||
>
|
||||
<FullScreenDropZone {...{ getDragAndDropRootProps }}>
|
||||
<UploadSelectorInputs
|
||||
getDragAndDropInputProps={getDragAndDropInputProps}
|
||||
getFileSelectorInputProps={getFileSelectorInputProps}
|
||||
getFolderSelectorInputProps={getFolderSelectorInputProps}
|
||||
getZipFileSelectorInputProps={undefined}
|
||||
{...{
|
||||
getDragAndDropInputProps,
|
||||
getFileSelectorInputProps,
|
||||
getFolderSelectorInputProps,
|
||||
}}
|
||||
/>
|
||||
<SharedAlbumNavbar
|
||||
showUploadButton={
|
||||
|
|
|
@ -133,17 +133,19 @@ some cases.
|
|||
|
||||
## Media
|
||||
|
||||
- ["jszip"](https://github.com/Stuk/jszip) is used for reading zip files in
|
||||
- [jszip](https://github.com/Stuk/jszip) is used for reading zip files in
|
||||
JavaScript (Live photos are zip files under the hood).
|
||||
|
||||
- ["file-type"](https://github.com/sindresorhus/file-type) is used for MIME
|
||||
type detection. We are at an old version 16.5.4 because v17 onwards the
|
||||
package became ESM only - for our limited use case, the custom Webpack
|
||||
configuration that entails is not worth the upgrade.
|
||||
- [file-type](https://github.com/sindresorhus/file-type) is used for MIME type
|
||||
detection. We are at an old version 16.5.4 because v17 onwards the package
|
||||
became ESM only - for our limited use case, the custom Webpack configuration
|
||||
that entails is not worth the upgrade.
|
||||
|
||||
## Photos app specific
|
||||
|
||||
### Misc
|
||||
- [react-dropzone](https://github.com/react-dropzone/react-dropzone/) is a
|
||||
React hook to create a drag-and-drop input zone.
|
||||
|
||||
- "sanitize-filename" is for converting arbitrary strings into strings that
|
||||
are suitable for being used as filenames.
|
||||
- [sanitize-filename](https://github.com/parshap/node-sanitize-filename) is
|
||||
for converting arbitrary strings into strings that are suitable for being
|
||||
used as filenames.
|
||||
|
|
|
@ -1,56 +1,71 @@
|
|||
import { useCallback, useRef, useState } from "react";
|
||||
|
||||
interface UseFileInputParams {
|
||||
/**
|
||||
* If `true`, the file open dialog will ask the user to select directories.
|
||||
* Otherwise it'll ask the user to select files (default).
|
||||
*/
|
||||
directory?: boolean;
|
||||
/**
|
||||
* If specified, it'll restrict the type of files that the user can select
|
||||
* by setting the "accept" attribute of the underlying HTML input element we
|
||||
* use to surface the file selector dialog. For value of accept can be an
|
||||
* extension or a MIME type (See
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept).
|
||||
*/
|
||||
accept?: string;
|
||||
}
|
||||
|
||||
interface UseFileInputResult {
|
||||
/**
|
||||
* A function to call to get the properties that should be passed to a dummy
|
||||
* `input` element that needs to be created to anchor the select file
|
||||
* dialog. This input HTML element is not going to be visible, but it needs
|
||||
* to be part of the DOM for {@link openSelector} to have effect.
|
||||
*/
|
||||
getInputProps: () => React.HTMLAttributes<HTMLInputElement>;
|
||||
/**
|
||||
* A function that can be called to open the select file / directory dialog.
|
||||
*/
|
||||
openSelector: () => void;
|
||||
/**
|
||||
* The list of {@link File}s that the user selected.
|
||||
*
|
||||
* This will be a list even if the user selected directories - in that case,
|
||||
* it will be the recursive list of files within this directory.
|
||||
*/
|
||||
selectedFiles: File[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return three things:
|
||||
* Wrap a open file selector into an easy to use package.
|
||||
*
|
||||
* - A function that can be called to trigger the showing of the select file /
|
||||
* directory dialog.
|
||||
* Returns a {@link UseFileInputResult} which contains a function to get the
|
||||
* props for an input element, a function to open the file selector, and the
|
||||
* list of selected files.
|
||||
*
|
||||
* - The list of properties that should be passed to a dummy `input` element
|
||||
* that needs to be created to anchor the select file dialog. This input HTML
|
||||
* element is not going to be visible, but it needs to be part of the DOM fro
|
||||
* the open trigger to have effect.
|
||||
*
|
||||
* - The list of files that the user selected. This will be a list even if the
|
||||
* user selected directories - in that case, it will be the recursive list of
|
||||
* files within this directory.
|
||||
*
|
||||
* @param param0
|
||||
*
|
||||
* - If {@link directory} is true, the file open dialog will ask the user to
|
||||
* select directories. Otherwise it'll ask the user to select files.
|
||||
*
|
||||
* - If {@link accept} is specified, it'll restrict the type of files that the
|
||||
* user can select by setting the "accept" attribute of the underlying HTML
|
||||
* input element we use to surface the file selector dialog. For value of
|
||||
* accept can be an extension or a MIME type (See
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept).
|
||||
* See the documentation of {@link UseFileInputParams} and
|
||||
* {@link UseFileInputResult} for more details.
|
||||
*/
|
||||
export default function useFileInput({
|
||||
export const useFileInput = ({
|
||||
directory,
|
||||
accept,
|
||||
}: UseFileInputParams) {
|
||||
}: UseFileInputParams): UseFileInputResult => {
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||
const inputRef = useRef<HTMLInputElement>();
|
||||
|
||||
const openSelectorDialog = useCallback(() => {
|
||||
const openSelector = useCallback(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = null;
|
||||
inputRef.current.click();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleChange: React.ChangeEventHandler<HTMLInputElement> = async (
|
||||
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (
|
||||
event,
|
||||
) => {
|
||||
if (!!event.target && !!event.target.files) {
|
||||
setSelectedFiles([...event.target.files]);
|
||||
}
|
||||
const files = event.target?.files;
|
||||
if (files) setSelectedFiles([...files]);
|
||||
};
|
||||
|
||||
// [Note: webkitRelativePath]
|
||||
|
@ -78,12 +93,8 @@ export default function useFileInput({
|
|||
onChange: handleChange,
|
||||
...(accept ? { accept } : {}),
|
||||
}),
|
||||
[],
|
||||
[directoryOpts, accept, handleChange],
|
||||
);
|
||||
|
||||
return {
|
||||
getInputProps,
|
||||
open: openSelectorDialog,
|
||||
selectedFiles: selectedFiles,
|
||||
};
|
||||
}
|
||||
return { getInputProps, openSelector, selectedFiles };
|
||||
};
|
||||
|
|
|
@ -13,3 +13,18 @@ export const shuffled = <T>(xs: T[]) =>
|
|||
.map((x) => [Math.random(), x])
|
||||
.sort()
|
||||
.map(([, x]) => x) as T[];
|
||||
|
||||
/**
|
||||
* Return the first non-empty string from the given list of strings.
|
||||
*
|
||||
* This function is needed because the `a ?? b` idiom doesn't do what you'd
|
||||
* expect when a is "". Perhaps the behaviour is wrong, perhaps the expecation
|
||||
* is wrong; this function papers over the differences.
|
||||
*
|
||||
* If none of the strings are non-empty, or if there are no strings in the given
|
||||
* array, return undefined.
|
||||
*/
|
||||
export const firstNonEmpty = (ss: (string | undefined)[]) => {
|
||||
for (const s of ss) if (s && s.length > 0) return s;
|
||||
return undefined;
|
||||
};
|
||||
|
|
|
@ -2503,12 +2503,12 @@ file-entry-cache@^6.0.1:
|
|||
dependencies:
|
||||
flat-cache "^3.0.4"
|
||||
|
||||
file-selector@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.4.0.tgz#59ec4f27aa5baf0841e9c6385c8386bef4d18b17"
|
||||
integrity sha512-iACCiXeMYOvZqlF1kTiYINzgepRBymz1wwjiuup9u9nayhb6g4fSwiyJ/6adli+EPwrWtpgQAh2PoS7HukEGEg==
|
||||
file-selector@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.6.0.tgz#fa0a8d9007b829504db4d07dd4de0310b65287dc"
|
||||
integrity sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==
|
||||
dependencies:
|
||||
tslib "^2.0.3"
|
||||
tslib "^2.4.0"
|
||||
|
||||
file-type@16.5.4:
|
||||
version "16.5.4"
|
||||
|
@ -3887,13 +3887,13 @@ react-dom@^18:
|
|||
loose-envify "^1.1.0"
|
||||
scheduler "^0.23.0"
|
||||
|
||||
react-dropzone@^11.2.4:
|
||||
version "11.7.1"
|
||||
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.7.1.tgz#3851bb75b26af0bf1b17ce1449fd980e643b9356"
|
||||
integrity sha512-zxCMwhfPy1olUEbw3FLNPLhAm/HnaYH5aELIEglRbqabizKAdHs0h+WuyOpmA+v1JXn0++fpQDdNfUagWt5hJQ==
|
||||
react-dropzone@^14.2:
|
||||
version "14.2.3"
|
||||
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.2.3.tgz#0acab68308fda2d54d1273a1e626264e13d4e84b"
|
||||
integrity sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==
|
||||
dependencies:
|
||||
attr-accept "^2.2.2"
|
||||
file-selector "^0.4.0"
|
||||
file-selector "^0.6.0"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
react-fast-compare@^2.0.1:
|
||||
|
@ -4592,7 +4592,7 @@ tsconfig-paths@^3.15.0:
|
|||
minimist "^1.2.6"
|
||||
strip-bom "^3.0.0"
|
||||
|
||||
tslib@^2.0.0, tslib@^2.0.3, tslib@^2.4.0, tslib@^2.6.2:
|
||||
tslib@^2.0.0, tslib@^2.4.0, tslib@^2.6.2:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
||||
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||
|
|
Loading…
Add table
Reference in a new issue