Inline and spruce

This commit is contained in:
Manav Rathi 2024-04-16 15:45:27 +05:30
parent 8fbff7e3a3
commit afec29d6e7
No known key found for this signature in database
9 changed files with 368 additions and 379 deletions

View file

@ -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";

View file

@ -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 {

View file

@ -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<WatchFolderProps> = ({ open, onClose }) => {
const [mappings, setMappings] = useState<WatchMapping[]>([]);
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 (
<>
<Dialog
open={open}
onClose={onClose}
PaperProps={{ sx: { height: "448px", maxWidth: "414px" } }}
>
<DialogTitleWithCloseButton
onClose={onClose}
sx={{ "&&&": { padding: "32px 16px 16px 24px" } }}
>
{t("WATCHED_FOLDERS")}
</DialogTitleWithCloseButton>
<DialogContent sx={{ flex: 1 }}>
<Stack spacing={1} p={1.5} height={"100%"}>
<MappingList
mappings={mappings}
handleRemoveWatchMapping={handleRemoveWatchMapping}
/>
<Button
fullWidth
color="accent"
onClick={handleAddFolderClick}
>
<span>+</span>
<span
style={{
marginLeft: "8px",
}}
></span>
{t("ADD_FOLDER")}
</Button>
</Stack>
</DialogContent>
</Dialog>
<UploadStrategyChoiceModal
open={choiceModalOpen}
onClose={closeChoiceModal}
uploadToSingleCollection={uploadToSingleCollection}
uploadToMultipleCollection={uploadToMultipleCollection}
/>
</>
);
};
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<MappingListProps> = ({
mappings,
handleRemoveWatchMapping,
}) => {
return mappings.length === 0 ? (
<NoMappingsContent />
) : (
<MappingsContainer>
{mappings.map((mapping) => {
return (
<MappingEntry
key={mapping.rootFolderName}
mapping={mapping}
handleRemoveMapping={handleRemoveWatchMapping}
/>
);
})}
</MappingsContainer>
);
};
const NoMappingsContent: React.FC = () => {
return (
<NoMappingsContainer>
<Stack spacing={1}>
<Typography variant="large" fontWeight={"bold"}>
{t("NO_FOLDERS_ADDED")}
</Typography>
<Typography py={0.5} variant={"small"} color="text.muted">
{t("FOLDERS_AUTOMATICALLY_MONITORED")}
</Typography>
<Typography variant={"small"} color="text.muted">
<FlexWrapper gap={1}>
<CheckmarkIcon />
{t("UPLOAD_NEW_FILES_TO_ENTE")}
</FlexWrapper>
</Typography>
<Typography variant={"small"} color="text.muted">
<FlexWrapper gap={1}>
<CheckmarkIcon />
{t("REMOVE_DELETED_FILES_FROM_ENTE")}
</FlexWrapper>
</Typography>
</Stack>
</NoMappingsContainer>
);
};
const CheckmarkIcon: React.FC = () => {
return (
<CheckIcon
fontSize="small"
sx={{
display: "inline",
fontSize: "15px",
color: (theme) => theme.palette.secondary.main,
}}
/>
);
};
interface MappingEntryProps {
mapping: WatchMapping;
handleRemoveMapping: (mapping: WatchMapping) => void;
}
const MappingEntry: React.FC<MappingEntryProps> = ({
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 (
<SpaceBetweenFlex>
<HorizontalFlex>
{mapping &&
mapping.uploadStrategy === UPLOAD_STRATEGY.SINGLE_COLLECTION ? (
<Tooltip title={t("UPLOADED_TO_SINGLE_COLLECTION")}>
<FolderOpenIcon />
</Tooltip>
) : (
<Tooltip title={t("UPLOADED_TO_SEPARATE_COLLECTIONS")}>
<FolderCopyOutlinedIcon />
</Tooltip>
)}
<EntryContainer>
<EntryHeading mapping={mapping} />
<Typography color="text.muted" variant="small">
{mapping.folderPath}
</Typography>
</EntryContainer>
</HorizontalFlex>
<MappingEntryOptions confirmStopWatching={confirmStopWatching} />
</SpaceBetweenFlex>
);
};
interface EntryHeadingProps {
mapping: WatchMapping;
}
const EntryHeading: React.FC<EntryHeadingProps> = ({ mapping }) => {
const appContext = useContext(AppContext);
return (
<FlexWrapper gap={1}>
<Typography>{mapping.rootFolderName}</Typography>
{appContext.isFolderSyncRunning &&
watchFolderService.isMappingSyncInProgress(mapping) && (
<CircularProgress size={12} />
)}
</FlexWrapper>
);
};
interface MappingEntryOptionsProps {
confirmStopWatching: () => void;
}
const MappingEntryOptions: React.FC<MappingEntryOptionsProps> = ({
confirmStopWatching,
}) => {
return (
<OverflowMenu
menuPaperProps={{
sx: {
backgroundColor: (theme) =>
theme.colors.background.elevated2,
},
}}
ariaControls={"watch-mapping-option"}
triggerButtonIcon={<MoreHorizIcon />}
>
<OverflowMenuOption
color="critical"
onClick={confirmStopWatching}
startIcon={<DoNotDisturbOutlinedIcon />}
>
{t("STOP_WATCHING")}
</OverflowMenuOption>
</OverflowMenu>
);
};

View file

@ -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<WatchMapping[]>([]);
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 (
<>
<Dialog
open={open}
onClose={onClose}
PaperProps={{ sx: { height: "448px", maxWidth: "414px" } }}
>
<DialogTitleWithCloseButton
onClose={onClose}
sx={{ "&&&": { padding: "32px 16px 16px 24px" } }}
>
{t("WATCHED_FOLDERS")}
</DialogTitleWithCloseButton>
<DialogContent sx={{ flex: 1 }}>
<Stack spacing={1} p={1.5} height={"100%"}>
<MappingList
mappings={mappings}
handleRemoveWatchMapping={handleRemoveWatchMapping}
/>
<Button
fullWidth
color="accent"
onClick={handleAddFolderClick}
>
<span>+</span>
<span
style={{
marginLeft: "8px",
}}
></span>
{t("ADD_FOLDER")}
</Button>
</Stack>
</DialogContent>
</Dialog>
<UploadStrategyChoiceModal
open={choiceModalOpen}
onClose={closeChoiceModal}
uploadToSingleCollection={uploadToSingleCollection}
uploadToMultipleCollection={uploadToMultipleCollection}
/>
</>
);
}

View file

@ -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 (
<SpaceBetweenFlex>
<HorizontalFlex>
{mapping &&
mapping.uploadStrategy === UPLOAD_STRATEGY.SINGLE_COLLECTION ? (
<Tooltip title={t("UPLOADED_TO_SINGLE_COLLECTION")}>
<FolderOpenIcon />
</Tooltip>
) : (
<Tooltip title={t("UPLOADED_TO_SEPARATE_COLLECTIONS")}>
<FolderCopyOutlinedIcon />
</Tooltip>
)}
<EntryContainer>
<EntryHeading mapping={mapping} />
<Typography color="text.muted" variant="small">
{mapping.folderPath}
</Typography>
</EntryContainer>
</HorizontalFlex>
<MappingEntryOptions confirmStopWatching={confirmStopWatching} />
</SpaceBetweenFlex>
);
}
interface EntryHeadingProps {
mapping: WatchMapping;
}
export function EntryHeading({ mapping }: EntryHeadingProps) {
const appContext = useContext(AppContext);
return (
<FlexWrapper gap={1}>
<Typography>{mapping.rootFolderName}</Typography>
{appContext.isFolderSyncRunning &&
watchFolderService.isMappingSyncInProgress(mapping) && (
<CircularProgress size={12} />
)}
</FlexWrapper>
);
}
interface MappingEntryOptionsProps {
confirmStopWatching: () => void;
}
export default function MappingEntryOptions({
confirmStopWatching,
}: MappingEntryOptionsProps) {
return (
<OverflowMenu
menuPaperProps={{
sx: {
backgroundColor: (theme) =>
theme.colors.background.elevated2,
},
}}
ariaControls={"watch-mapping-option"}
triggerButtonIcon={<MoreHorizIcon />}
>
<OverflowMenuOption
color="critical"
onClick={confirmStopWatching}
startIcon={<DoNotDisturbOutlinedIcon />}
>
{t("STOP_WATCHING")}
</OverflowMenuOption>
</OverflowMenu>
);
}

View file

@ -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 ? (
<NoMappingsContent />
) : (
<MappingsContainer>
{mappings.map((mapping) => {
return (
<MappingEntry
key={mapping.rootFolderName}
mapping={mapping}
handleRemoveMapping={handleRemoveWatchMapping}
/>
);
})}
</MappingsContainer>
);
}
export function NoMappingsContent() {
return (
<NoMappingsContainer>
<Stack spacing={1}>
<Typography variant="large" fontWeight={"bold"}>
{t("NO_FOLDERS_ADDED")}
</Typography>
<Typography py={0.5} variant={"small"} color="text.muted">
{t("FOLDERS_AUTOMATICALLY_MONITORED")}
</Typography>
<Typography variant={"small"} color="text.muted">
<FlexWrapper gap={1}>
<CheckmarkIcon />
{t("UPLOAD_NEW_FILES_TO_ENTE")}
</FlexWrapper>
</Typography>
<Typography variant={"small"} color="text.muted">
<FlexWrapper gap={1}>
<CheckmarkIcon />
{t("REMOVE_DELETED_FILES_FROM_ENTE")}
</FlexWrapper>
</Typography>
</Stack>
</NoMappingsContainer>
);
}
export function CheckmarkIcon() {
return (
<CheckIcon
fontSize="small"
sx={{
display: "inline",
fontSize: "15px",
color: (theme) => theme.palette.secondary.main,
}}
/>
);
}

View file

@ -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",
});

View file

@ -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";

View file

@ -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[] = [];