Files
leonpan-assets/src/component/FileManager/Dialogs/Share/ShareDialog.tsx

279 lines
9.8 KiB
TypeScript
Raw Normal View History

2025-10-19 13:31:11 +00:00
import { Box, Checkbox, Collapse, DialogContent, IconButton, Stack, Tooltip, useTheme } from "@mui/material";
import dayjs from "dayjs";
import { TFunction } from "i18next";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { CSSTransition, SwitchTransition } from "react-transition-group";
import { Share as ShareModel } from "../../../../api/explorer.ts";
import { closeShareLinkDialog } from "../../../../redux/globalStateSlice.ts";
import { useAppDispatch, useAppSelector } from "../../../../redux/hooks.ts";
import { createOrUpdateShareLink } from "../../../../redux/thunks/share.ts";
import { copyToClipboard, sendLink } from "../../../../util";
import AutoHeight from "../../../Common/AutoHeight.tsx";
import { FilledTextField, SmallFormControlLabel } from "../../../Common/StyledComponents.tsx";
import DraggableDialog from "../../../Dialogs/DraggableDialog.tsx";
import CopyOutlined from "../../../Icons/CopyOutlined.tsx";
import Share from "../../../Icons/Share.tsx";
import { FileManagerIndex } from "../../FileManager.tsx";
import ShareSettingContent, { downloadOptions, expireOptions, ShareSetting } from "./ShareSetting.tsx";
const initialSetting: ShareSetting = {
expires_val: expireOptions[2],
downloads_val: downloadOptions[0],
};
interface ShareLinkPassword {
shareLink: string;
password?: string;
}
const shareToSetting = (share: ShareModel, t: TFunction): ShareSetting => {
const res: ShareSetting = {
is_private: share.is_private,
password: share.password,
use_custom_password: true,
share_view: share.share_view,
show_readme: share.show_readme,
downloads: share.remain_downloads != undefined && share.remain_downloads > 0,
expires_val: expireOptions[2],
downloads_val: downloadOptions[0],
};
if (res.downloads) {
res.downloads_val = {
value: share.remain_downloads ?? 0,
label: (share.remain_downloads ?? 0).toString(),
};
}
if (share.expires != undefined) {
const expires = dayjs(share.expires);
const isExpired = expires.isBefore(dayjs());
if (!isExpired) {
res.expires = true;
const secondsTtl = dayjs(share.expires).diff(dayjs(), "second");
res.expires_val = {
value: secondsTtl,
label: Math.round(secondsTtl / 60) + " " + t("application:modals.minutes"),
};
} else {
res.expires = false;
}
}
return res;
};
const ShareDialog = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const theme = useTheme();
const [loading, setLoading] = useState(false);
const [setting, setSetting] = useState<ShareSetting>(initialSetting);
const [shareLink, setShareLink] = useState<string>("");
const [includePassword, setIncludePassword] = useState(true);
const shareLinkPassword = useMemo(() => {
const start = shareLink.lastIndexOf("/s/");
const shareLinkParts = shareLink.substring(start + 3).split("/");
const password = shareLinkParts.length == 2 ? shareLinkParts[1] : undefined;
return {
shareLink: password ? shareLink.substring(0, shareLink.lastIndexOf("/")) : shareLink,
password: password,
} as ShareLinkPassword;
}, [shareLink]);
const open = useAppSelector((state) => state.globalState.shareLinkDialogOpen);
const target = useAppSelector((state) => state.globalState.shareLinkDialogFile);
const editTarget = useAppSelector((state) => state.globalState.shareLinkDialogShare);
useEffect(() => {
if (open) {
if (editTarget) {
setSetting(shareToSetting(editTarget, t));
} else {
setSetting(initialSetting);
}
setShareLink("");
setIncludePassword(true);
}
}, [open]);
const onClose = useCallback(() => {
if (!loading) {
dispatch(closeShareLinkDialog());
}
}, [dispatch, loading]);
const onAccept = useCallback(
async (e?: React.MouseEvent<HTMLElement>) => {
if (e) {
e.preventDefault();
}
if (!target) return;
if (shareLink) {
copyToClipboard(shareLink);
return;
}
setLoading(true);
try {
const shareLink = await dispatch(
createOrUpdateShareLink(FileManagerIndex.main, target, setting, editTarget?.id),
);
setShareLink(shareLink);
} catch (e) {
} finally {
setLoading(false);
}
},
[dispatch, target, shareLink, editTarget, setLoading, setting, setShareLink],
);
const finalShareLink = useMemo(() => {
if (includePassword) {
return shareLink;
}
return shareLink.substring(0, shareLink.lastIndexOf("/"));
}, [includePassword, shareLink]);
const finalShareLinkPassword = useMemo(() => {
if (!includePassword) {
return shareLink.substring(shareLink.lastIndexOf("/") + 1);
}
return undefined;
}, [includePassword, shareLink]);
return (
<>
<DraggableDialog
title={t(`application:modals.${editTarget ? "edit" : "create"}ShareLink`)}
showActions
loading={loading}
showCancel
hideOk={!!shareLink}
onAccept={onAccept}
dialogProps={{
open: open ?? false,
onClose: onClose,
fullWidth: true,
maxWidth: "xs",
}}
cancelText={shareLink ? t("common:close") : undefined}
secondaryAction={
shareLink
? // @ts-ignore
navigator.share && (
<Tooltip title={t("application:modals.sendLink")}>
<IconButton onClick={() => sendLink(target?.name ?? "", finalShareLink)}>
<Share />
</IconButton>
</Tooltip>
)
: undefined
}
>
<DialogContent sx={{ pb: 0 }}>
<AutoHeight>
<SwitchTransition>
<CSSTransition
addEndListener={(node, done) => node.addEventListener("transitionend", done, false)}
classNames="fade"
key={`${shareLink}`}
>
<Box>
{!shareLink && (
<ShareSettingContent
editing={!!editTarget}
onSettingChange={setSetting}
setting={setting}
file={target}
/>
)}
{shareLink && (
<Stack spacing={1}>
<FilledTextField
variant={"filled"}
inputProps={{ readonly: true }}
label={t("modals.shareLink")}
fullWidth
value={finalShareLink ?? ""}
onFocus={(e) => e.target.select()}
slotProps={{
input: {
endAdornment: (
<IconButton
onClick={() => copyToClipboard(finalShareLink)}
size="small"
sx={{ marginRight: -1 }}
>
<CopyOutlined />
</IconButton>
),
},
}}
/>
{shareLinkPassword.password && (
<>
<Collapse in={!includePassword}>
<FilledTextField
variant={"filled"}
inputProps={{ readonly: true }}
label={t("modals.sharePassword")}
fullWidth
value={finalShareLinkPassword ?? ""}
onFocus={(e) => e.target.select()}
slotProps={{
input: {
endAdornment: (
<IconButton
onClick={() => copyToClipboard(finalShareLinkPassword ?? "")}
size="small"
sx={{ marginRight: -1 }}
>
<CopyOutlined />
</IconButton>
),
},
}}
/>
</Collapse>
<Tooltip enterDelay={100} title={t("application:modals.includePasswordInShareLinkDes")}>
<SmallFormControlLabel
sx={{
mt: "0!important",
}}
control={
<Checkbox
disableRipple
sx={{
pl: 0,
}}
size="small"
checked={includePassword}
onChange={() => {
setIncludePassword(!includePassword);
}}
/>
}
label={t("application:modals.includePasswordInShareLink")}
/>
</Tooltip>
</>
)}
</Stack>
)}
</Box>
</CSSTransition>
</SwitchTransition>
</AutoHeight>
</DialogContent>
</DraggableDialog>
</>
);
};
export default ShareDialog;