diff --git a/web/apps/photos/src/components/Sidebar/UtilitySection.tsx b/web/apps/photos/src/components/Sidebar/UtilitySection.tsx index 904eab747..c9c734cd9 100644 --- a/web/apps/photos/src/components/Sidebar/UtilitySection.tsx +++ b/web/apps/photos/src/components/Sidebar/UtilitySection.tsx @@ -24,7 +24,7 @@ import { import { getAccountsURL } from "@ente/shared/network/api"; import { THEME_COLOR } from "@ente/shared/themes/constants"; import { EnteMenuItem } from "components/Menu/EnteMenuItem"; -import WatchFolder from "components/WatchFolder"; +import { WatchFolder } from "components/WatchFolder"; import isElectron from "is-electron"; import { getAccountsToken } from "services/userService"; import { getDownloadAppMessage } from "utils/ui"; diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index 4d81b1612..bb3d4fd9d 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -24,7 +24,7 @@ import { savePublicCollectionUploaderName, } from "services/publicCollectionService"; import uploadManager from "services/upload/uploadManager"; -import watchFolderService from "services/watchFolder/watchFolderService"; +import watchFolderService from "services/watch"; import { NotificationAttributes } from "types/Notification"; import { Collection } from "types/collection"; import { diff --git a/web/apps/photos/src/components/WatchFolder.tsx b/web/apps/photos/src/components/WatchFolder.tsx new file mode 100644 index 000000000..8001dd992 --- /dev/null +++ b/web/apps/photos/src/components/WatchFolder.tsx @@ -0,0 +1,363 @@ +import { + FlexWrapper, + HorizontalFlex, + SpaceBetweenFlex, + VerticallyCentered, +} from "@ente/shared/components/Container"; +import DialogTitleWithCloseButton from "@ente/shared/components/DialogBox/TitleWithCloseButton"; +import OverflowMenu from "@ente/shared/components/OverflowMenu/menu"; +import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option"; +import CheckIcon from "@mui/icons-material/Check"; +import DoNotDisturbOutlinedIcon from "@mui/icons-material/DoNotDisturbOutlined"; +import FolderCopyOutlinedIcon from "@mui/icons-material/FolderCopyOutlined"; +import FolderOpenIcon from "@mui/icons-material/FolderOpen"; +import MoreHorizIcon from "@mui/icons-material/MoreHoriz"; +import { + Box, + Button, + CircularProgress, + Dialog, + DialogContent, + Stack, + Tooltip, + Typography, +} from "@mui/material"; +import { styled } from "@mui/material/styles"; +import UploadStrategyChoiceModal from "components/Upload/UploadStrategyChoiceModal"; +import { PICKED_UPLOAD_TYPE, UPLOAD_STRATEGY } from "constants/upload"; +import { t } from "i18next"; +import { AppContext } from "pages/_app"; +import React, { useContext, useEffect, useState } from "react"; +import watchFolderService from "services/watch"; +import { WatchMapping } from "types/watchFolder"; +import { getImportSuggestion } from "utils/upload"; + +interface WatchFolderProps { + open: boolean; + onClose: () => void; +} + +export const WatchFolder: React.FC = ({ open, onClose }) => { + const [mappings, setMappings] = useState([]); + const [inputFolderPath, setInputFolderPath] = useState(""); + const [choiceModalOpen, setChoiceModalOpen] = useState(false); + const appContext = useContext(AppContext); + + const electron = globalThis.electron; + + useEffect(() => { + if (!electron) return; + watchFolderService.getWatchMappings().then((m) => setMappings(m)); + }, []); + + useEffect(() => { + if ( + appContext.watchFolderFiles && + appContext.watchFolderFiles.length > 0 + ) { + handleFolderDrop(appContext.watchFolderFiles); + appContext.setWatchFolderFiles(null); + } + }, [appContext.watchFolderFiles]); + + const handleFolderDrop = async (folders: FileList) => { + for (let i = 0; i < folders.length; i++) { + const folder: any = folders[i]; + const path = (folder.path as string).replace(/\\/g, "/"); + if (await watchFolderService.isFolder(path)) { + await addFolderForWatching(path); + } + } + }; + + const addFolderForWatching = async (path: string) => { + if (!electron) return; + + setInputFolderPath(path); + const files = await electron.getDirFiles(path); + const analysisResult = getImportSuggestion( + PICKED_UPLOAD_TYPE.FOLDERS, + files, + ); + if (analysisResult.hasNestedFolders) { + setChoiceModalOpen(true); + } else { + handleAddWatchMapping(UPLOAD_STRATEGY.SINGLE_COLLECTION, path); + } + }; + + const handleAddFolderClick = async () => { + await handleFolderSelection(); + }; + + const handleFolderSelection = async () => { + const folderPath = await watchFolderService.selectFolder(); + if (folderPath) { + await addFolderForWatching(folderPath); + } + }; + + const handleAddWatchMapping = async ( + uploadStrategy: UPLOAD_STRATEGY, + folderPath?: string, + ) => { + folderPath = folderPath || inputFolderPath; + await watchFolderService.addWatchMapping( + folderPath.substring(folderPath.lastIndexOf("/") + 1), + folderPath, + uploadStrategy, + ); + setInputFolderPath(""); + setMappings(await watchFolderService.getWatchMappings()); + }; + + const handleRemoveWatchMapping = async (mapping: WatchMapping) => { + await watchFolderService.removeWatchMapping(mapping.folderPath); + setMappings(await watchFolderService.getWatchMappings()); + }; + + const closeChoiceModal = () => setChoiceModalOpen(false); + + const uploadToSingleCollection = () => { + closeChoiceModal(); + handleAddWatchMapping(UPLOAD_STRATEGY.SINGLE_COLLECTION); + }; + + const uploadToMultipleCollection = () => { + closeChoiceModal(); + handleAddWatchMapping(UPLOAD_STRATEGY.COLLECTION_PER_FOLDER); + }; + + return ( + <> + + + {t("WATCHED_FOLDERS")} + + + + + + + + + + + ); +}; + +const MappingsContainer = styled(Box)(() => ({ + height: "278px", + overflow: "auto", + "&::-webkit-scrollbar": { + width: "4px", + }, +})); + +const NoMappingsContainer = styled(VerticallyCentered)({ + textAlign: "left", + alignItems: "flex-start", + marginBottom: "32px", +}); + +const EntryContainer = styled(Box)({ + marginLeft: "12px", + marginRight: "6px", + marginBottom: "12px", +}); + +interface MappingListProps { + mappings: WatchMapping[]; + handleRemoveWatchMapping: (value: WatchMapping) => void; +} + +const MappingList: React.FC = ({ + mappings, + handleRemoveWatchMapping, +}) => { + return mappings.length === 0 ? ( + + ) : ( + + {mappings.map((mapping) => { + return ( + + ); + })} + + ); +}; + +const NoMappingsContent: React.FC = () => { + return ( + + + + {t("NO_FOLDERS_ADDED")} + + + {t("FOLDERS_AUTOMATICALLY_MONITORED")} + + + + + {t("UPLOAD_NEW_FILES_TO_ENTE")} + + + + + + {t("REMOVE_DELETED_FILES_FROM_ENTE")} + + + + + ); +}; + +const CheckmarkIcon: React.FC = () => { + return ( + theme.palette.secondary.main, + }} + /> + ); +}; + +interface MappingEntryProps { + mapping: WatchMapping; + handleRemoveMapping: (mapping: WatchMapping) => void; +} + +const MappingEntry: React.FC = ({ + mapping, + handleRemoveMapping, +}) => { + const appContext = React.useContext(AppContext); + + const stopWatching = () => { + handleRemoveMapping(mapping); + }; + + const confirmStopWatching = () => { + appContext.setDialogMessage({ + title: t("STOP_WATCHING_FOLDER"), + content: t("STOP_WATCHING_DIALOG_MESSAGE"), + close: { + text: t("CANCEL"), + variant: "secondary", + }, + proceed: { + action: stopWatching, + text: t("YES_STOP"), + variant: "critical", + }, + }); + }; + + return ( + + + {mapping && + mapping.uploadStrategy === UPLOAD_STRATEGY.SINGLE_COLLECTION ? ( + + + + ) : ( + + + + )} + + + + {mapping.folderPath} + + + + + + ); +}; + +interface EntryHeadingProps { + mapping: WatchMapping; +} + +const EntryHeading: React.FC = ({ mapping }) => { + const appContext = useContext(AppContext); + return ( + + {mapping.rootFolderName} + {appContext.isFolderSyncRunning && + watchFolderService.isMappingSyncInProgress(mapping) && ( + + )} + + ); +}; + +interface MappingEntryOptionsProps { + confirmStopWatching: () => void; +} + +const MappingEntryOptions: React.FC = ({ + confirmStopWatching, +}) => { + return ( + + theme.colors.background.elevated2, + }, + }} + ariaControls={"watch-mapping-option"} + triggerButtonIcon={} + > + } + > + {t("STOP_WATCHING")} + + + ); +}; diff --git a/web/apps/photos/src/components/WatchFolder/index.tsx b/web/apps/photos/src/components/WatchFolder/index.tsx deleted file mode 100644 index 4ccfd4138..000000000 --- a/web/apps/photos/src/components/WatchFolder/index.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import DialogTitleWithCloseButton from "@ente/shared/components/DialogBox/TitleWithCloseButton"; -import { Button, Dialog, DialogContent, Stack } from "@mui/material"; -import UploadStrategyChoiceModal from "components/Upload/UploadStrategyChoiceModal"; -import { PICKED_UPLOAD_TYPE, UPLOAD_STRATEGY } from "constants/upload"; -import { t } from "i18next"; -import { AppContext } from "pages/_app"; -import { useContext, useEffect, useState } from "react"; -import watchFolderService from "services/watchFolder/watchFolderService"; -import { WatchMapping } from "types/watchFolder"; -import { getImportSuggestion } from "utils/upload"; -import { MappingList } from "./mappingList"; - -interface Iprops { - open: boolean; - onClose: () => void; -} - -export default function WatchFolder({ open, onClose }: Iprops) { - const [mappings, setMappings] = useState([]); - const [inputFolderPath, setInputFolderPath] = useState(""); - const [choiceModalOpen, setChoiceModalOpen] = useState(false); - const appContext = useContext(AppContext); - - const electron = globalThis.electron; - - useEffect(() => { - if (!electron) return; - watchFolderService.getWatchMappings().then((m) => setMappings(m)); - }, []); - - useEffect(() => { - if ( - appContext.watchFolderFiles && - appContext.watchFolderFiles.length > 0 - ) { - handleFolderDrop(appContext.watchFolderFiles); - appContext.setWatchFolderFiles(null); - } - }, [appContext.watchFolderFiles]); - - const handleFolderDrop = async (folders: FileList) => { - for (let i = 0; i < folders.length; i++) { - const folder: any = folders[i]; - const path = (folder.path as string).replace(/\\/g, "/"); - if (await watchFolderService.isFolder(path)) { - await addFolderForWatching(path); - } - } - }; - - const addFolderForWatching = async (path: string) => { - if (!electron) return; - - setInputFolderPath(path); - const files = await electron.getDirFiles(path); - const analysisResult = getImportSuggestion( - PICKED_UPLOAD_TYPE.FOLDERS, - files, - ); - if (analysisResult.hasNestedFolders) { - setChoiceModalOpen(true); - } else { - handleAddWatchMapping(UPLOAD_STRATEGY.SINGLE_COLLECTION, path); - } - }; - - const handleAddFolderClick = async () => { - await handleFolderSelection(); - }; - - const handleFolderSelection = async () => { - const folderPath = await watchFolderService.selectFolder(); - if (folderPath) { - await addFolderForWatching(folderPath); - } - }; - - const handleAddWatchMapping = async ( - uploadStrategy: UPLOAD_STRATEGY, - folderPath?: string, - ) => { - folderPath = folderPath || inputFolderPath; - await watchFolderService.addWatchMapping( - folderPath.substring(folderPath.lastIndexOf("/") + 1), - folderPath, - uploadStrategy, - ); - setInputFolderPath(""); - setMappings(await watchFolderService.getWatchMappings()); - }; - - const handleRemoveWatchMapping = async (mapping: WatchMapping) => { - await watchFolderService.removeWatchMapping(mapping.folderPath); - setMappings(await watchFolderService.getWatchMappings()); - }; - - const closeChoiceModal = () => setChoiceModalOpen(false); - - const uploadToSingleCollection = () => { - closeChoiceModal(); - handleAddWatchMapping(UPLOAD_STRATEGY.SINGLE_COLLECTION); - }; - - const uploadToMultipleCollection = () => { - closeChoiceModal(); - handleAddWatchMapping(UPLOAD_STRATEGY.COLLECTION_PER_FOLDER); - }; - - return ( - <> - - - {t("WATCHED_FOLDERS")} - - - - - - - - - - - ); -} diff --git a/web/apps/photos/src/components/WatchFolder/mappingEntry/index.tsx b/web/apps/photos/src/components/WatchFolder/mappingEntry/index.tsx deleted file mode 100644 index c51184866..000000000 --- a/web/apps/photos/src/components/WatchFolder/mappingEntry/index.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { - FlexWrapper, - HorizontalFlex, - SpaceBetweenFlex, -} from "@ente/shared/components/Container"; -import OverflowMenu from "@ente/shared/components/OverflowMenu/menu"; -import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option"; -import DoNotDisturbOutlinedIcon from "@mui/icons-material/DoNotDisturbOutlined"; -import FolderCopyOutlinedIcon from "@mui/icons-material/FolderCopyOutlined"; -import FolderOpenIcon from "@mui/icons-material/FolderOpen"; -import MoreHorizIcon from "@mui/icons-material/MoreHoriz"; -import { CircularProgress, Tooltip, Typography } from "@mui/material"; -import { UPLOAD_STRATEGY } from "constants/upload"; -import { t } from "i18next"; -import { AppContext } from "pages/_app"; -import React, { useContext } from "react"; -import watchFolderService from "services/watchFolder/watchFolderService"; -import { WatchMapping } from "types/watchFolder"; -import { EntryContainer } from "../styledComponents"; -import { EntryHeading } from "./entryHeading"; -import MappingEntryOptions from "./mappingEntryOptions"; - -interface MappingEntryProps { - mapping: WatchMapping; - handleRemoveMapping: (mapping: WatchMapping) => void; -} - -export function MappingEntry({ - mapping, - handleRemoveMapping, -}: MappingEntryProps) { - const appContext = React.useContext(AppContext); - - const stopWatching = () => { - handleRemoveMapping(mapping); - }; - - const confirmStopWatching = () => { - appContext.setDialogMessage({ - title: t("STOP_WATCHING_FOLDER"), - content: t("STOP_WATCHING_DIALOG_MESSAGE"), - close: { - text: t("CANCEL"), - variant: "secondary", - }, - proceed: { - action: stopWatching, - text: t("YES_STOP"), - variant: "critical", - }, - }); - }; - - return ( - - - {mapping && - mapping.uploadStrategy === UPLOAD_STRATEGY.SINGLE_COLLECTION ? ( - - - - ) : ( - - - - )} - - - - {mapping.folderPath} - - - - - - ); -} - -interface EntryHeadingProps { - mapping: WatchMapping; -} - -export function EntryHeading({ mapping }: EntryHeadingProps) { - const appContext = useContext(AppContext); - return ( - - {mapping.rootFolderName} - {appContext.isFolderSyncRunning && - watchFolderService.isMappingSyncInProgress(mapping) && ( - - )} - - ); -} - -interface MappingEntryOptionsProps { - confirmStopWatching: () => void; -} - -export default function MappingEntryOptions({ - confirmStopWatching, -}: MappingEntryOptionsProps) { - return ( - - theme.colors.background.elevated2, - }, - }} - ariaControls={"watch-mapping-option"} - triggerButtonIcon={} - > - } - > - {t("STOP_WATCHING")} - - - ); -} diff --git a/web/apps/photos/src/components/WatchFolder/mappingList/index.tsx b/web/apps/photos/src/components/WatchFolder/mappingList/index.tsx deleted file mode 100644 index 97d2f56ca..000000000 --- a/web/apps/photos/src/components/WatchFolder/mappingList/index.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { FlexWrapper } from "@ente/shared/components/Container"; -import CheckIcon from "@mui/icons-material/Check"; -import { Stack, Typography } from "@mui/material"; -import { t } from "i18next"; -import { WatchMapping } from "types/watchFolder"; -import { NoMappingsContainer } from "../../styledComponents"; -import { MappingEntry } from "../mappingEntry"; -import { MappingsContainer } from "../styledComponents"; -import { NoMappingsContent } from "./noMappingsContent/noMappingsContent"; - -interface MappingListProps { - mappings: WatchMapping[]; - handleRemoveWatchMapping: (value: WatchMapping) => void; -} - -export function MappingList({ - mappings, - handleRemoveWatchMapping, -}: MappingListProps) { - return mappings.length === 0 ? ( - - ) : ( - - {mappings.map((mapping) => { - return ( - - ); - })} - - ); -} - -export function NoMappingsContent() { - return ( - - - - {t("NO_FOLDERS_ADDED")} - - - {t("FOLDERS_AUTOMATICALLY_MONITORED")} - - - - - {t("UPLOAD_NEW_FILES_TO_ENTE")} - - - - - - {t("REMOVE_DELETED_FILES_FROM_ENTE")} - - - - - ); -} - -export function CheckmarkIcon() { - return ( - theme.palette.secondary.main, - }} - /> - ); -} diff --git a/web/apps/photos/src/components/WatchFolder/styledComponents.tsx b/web/apps/photos/src/components/WatchFolder/styledComponents.tsx deleted file mode 100644 index d507bbaa8..000000000 --- a/web/apps/photos/src/components/WatchFolder/styledComponents.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { VerticallyCentered } from "@ente/shared/components/Container"; -import { Box } from "@mui/material"; -import { styled } from "@mui/material/styles"; - -export const MappingsContainer = styled(Box)(() => ({ - height: "278px", - overflow: "auto", - "&::-webkit-scrollbar": { - width: "4px", - }, -})); - -export const NoMappingsContainer = styled(VerticallyCentered)({ - textAlign: "left", - alignItems: "flex-start", - marginBottom: "32px", -}); - -export const EntryContainer = styled(Box)({ - marginLeft: "12px", - marginRight: "6px", - marginBottom: "12px", -}); diff --git a/web/apps/photos/src/services/upload/uploadManager.ts b/web/apps/photos/src/services/upload/uploadManager.ts index 82b761091..d222999d8 100644 --- a/web/apps/photos/src/services/upload/uploadManager.ts +++ b/web/apps/photos/src/services/upload/uploadManager.ts @@ -14,7 +14,7 @@ import { getPublicCollectionUID, } from "services/publicCollectionService"; import { getDisableCFUploadProxyFlag } from "services/userService"; -import watchFolderService from "services/watchFolder/watchFolderService"; +import watchFolderService from "services/watch"; import { Collection } from "types/collection"; import { EncryptedEnteFile, EnteFile } from "types/file"; import { SetFiles } from "types/gallery"; diff --git a/web/apps/photos/src/services/watchFolder/watchFolderService.ts b/web/apps/photos/src/services/watch.ts similarity index 99% rename from web/apps/photos/src/services/watchFolder/watchFolderService.ts rename to web/apps/photos/src/services/watch.ts index e039eb3de..1c26addf0 100644 --- a/web/apps/photos/src/services/watchFolder/watchFolderService.ts +++ b/web/apps/photos/src/services/watch.ts @@ -13,8 +13,8 @@ import { } from "types/watchFolder"; import { groupFilesBasedOnCollectionID } from "utils/file"; import { isSystemFile } from "utils/upload"; -import { removeFromCollection } from "../collectionService"; -import { getLocalFiles } from "../fileService"; +import { removeFromCollection } from "./collectionService"; +import { getLocalFiles } from "./fileService"; class WatchFolderService { private eventQueue: EventQueueItem[] = [];