first commit

This commit is contained in:
2025-10-19 13:31:11 +00:00
commit 8bfc183b66
1248 changed files with 195992 additions and 0 deletions

View File

@@ -0,0 +1,543 @@
import {
DialogContent,
FormControlLabel,
IconButton,
Link,
ListItemText,
SelectChangeEvent,
Switch,
Table,
TableBody,
TableContainer,
TableHead,
TableRow,
useTheme,
} from "@mui/material";
import FormControl from "@mui/material/FormControl";
import Grid from "@mui/material/Grid2";
import { useSnackbar } from "notistack";
import React, { lazy, Suspense, useCallback, useEffect, useMemo, useState } from "react";
import { DndProvider, useDrag, useDrop } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { Trans, useTranslation } from "react-i18next";
import { Viewer, ViewerPlatform, ViewerType } from "../../../../api/explorer.ts";
import { builtInViewers } from "../../../../redux/thunks/viewer.ts";
import { isTrueVal } from "../../../../session/utils.ts";
import CircularProgress from "../../../Common/CircularProgress.tsx";
import SizeInput from "../../../Common/SizeInput.tsx";
import { DefaultCloseAction } from "../../../Common/Snackbar/snackbar.tsx";
import {
DenseFilledTextField,
DenseSelect,
NoWrapTableCell,
SecondaryButton,
StyledTableContainerPaper,
} from "../../../Common/StyledComponents.tsx";
import DraggableDialog from "../../../Dialogs/DraggableDialog.tsx";
import { SquareMenuItem } from "../../../FileManager/ContextMenu/ContextMenu.tsx";
import { ViewerIDWithDefaultIcons } from "../../../FileManager/Dialogs/OpenWith.tsx";
import Add from "../../../Icons/Add.tsx";
import ArrowDown from "../../../Icons/ArrowDown.tsx";
import Dismiss from "../../../Icons/Dismiss.tsx";
import SettingForm from "../../../Pages/Setting/SettingForm.tsx";
import MagicVarDialog, { MagicVar } from "../../Common/MagicVarDialog.tsx";
import { NoMarginHelperText } from "../../Settings/Settings.tsx";
const MonacoEditor = lazy(() => import("../../../Viewers/CodeViewer/MonacoEditor.tsx"));
export interface FileViewerEditDialogProps {
viewer: Viewer;
onChange: (viewer: Viewer) => void;
open: boolean;
onClose: () => void;
}
const magicVars: MagicVar[] = [
{
name: "{$src}",
value: "settings.srcEncodedVar",
example: "https%3A%2F%2Fcloudreve.org%2Fapi%2Fv4%2Ffile%2Fcontent%2FzOie%2F0%2Ftext.txt%3Fsign%3Dxxx",
},
{
name: "{$src_raw}",
value: "settings.srcVar",
example: "https://cloudreve.org/api/v4/file/content/zOie/0/text.txt?sign=xxx",
},
{
name: "{$src_raw_base64}",
value: "settings.srcBase64Var",
example: "aHR0cHM6Ly9jbG91ZHJldmUub3JnL2FwaS92NC9maWxlL2NvbnRlbnQvek9pZS8wL3RleHQudHh0P3NpZ249eHh4",
},
{
name: "{$name}",
value: "settings.nameEncodedVar",
example: "sampleFile%5B1%5D.txt",
},
{
name: "{$version}",
value: "settings.versionEntityVar",
example: "zOie",
},
{
name: "{$id}",
value: "settings.fileIdVar",
example: "jm8AF8",
},
{
name: "{$user_id}",
value: "settings.userIdVar",
example: "lpua",
},
{
name: "{$user_display_name}",
value: "settings.userDisplayNameVar",
example: "Aaron%20Liu",
},
];
const DND_TYPE = "template-row";
interface DraggableTemplateRowProps {
t: any;
i: number;
moveRow: (from: number, to: number) => void;
onExtChange: (e: SelectChangeEvent<unknown>, child: React.ReactNode) => void;
onNameChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onDelete: () => void;
isFirst: boolean;
isLast: boolean;
extList: string[];
template: any;
}
function DraggableTemplateRow({
i,
moveRow,
onExtChange,
onNameChange,
onDelete,
isFirst,
isLast,
extList,
template,
}: DraggableTemplateRowProps) {
const ref = React.useRef<HTMLTableRowElement>(null);
const [, drop] = useDrop({
accept: DND_TYPE,
hover(item: any, monitor) {
if (!ref.current) return;
const dragIndex = item.index;
const hoverIndex = i;
if (dragIndex === hoverIndex) return;
const hoverBoundingRect = ref.current.getBoundingClientRect();
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
const clientOffset = monitor.getClientOffset();
if (!clientOffset) return;
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) return;
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) return;
moveRow(dragIndex, hoverIndex);
item.index = hoverIndex;
},
});
const [{ isDragging }, drag] = useDrag({
type: DND_TYPE,
item: { index: i },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
drag(drop(ref));
return (
<TableRow
ref={ref}
sx={{ "&:last-child td, &:last-child th": { border: 0 }, opacity: isDragging ? 0.5 : 1, cursor: "move" }}
hover
>
<NoWrapTableCell>
<DenseSelect value={template.ext} required onChange={onExtChange}>
{extList.map((ext) => (
<SquareMenuItem value={ext} key={ext}>
<ListItemText slotProps={{ primary: { variant: "body2" } }}>{ext}</ListItemText>
</SquareMenuItem>
))}
</DenseSelect>
</NoWrapTableCell>
<NoWrapTableCell>
<DenseFilledTextField fullWidth required value={template.display_name} onChange={onNameChange} />
</NoWrapTableCell>
<NoWrapTableCell>
<IconButton size={"small"} onClick={onDelete}>
<Dismiss fontSize={"small"} />
</IconButton>
<IconButton size="small" onClick={() => moveRow(i, i - 1)} disabled={isFirst}>
<ArrowDown
sx={{
width: "18px",
height: "18px",
transform: "rotate(180deg)",
}}
/>
</IconButton>
<IconButton size="small" onClick={() => moveRow(i, i + 1)} disabled={isLast}>
<ArrowDown
sx={{
width: "18px",
height: "18px",
}}
/>
</IconButton>
</NoWrapTableCell>
</TableRow>
);
}
const FileViewerEditDialog = ({ viewer, onChange, open, onClose }: FileViewerEditDialogProps) => {
const { t } = useTranslation("dashboard");
const theme = useTheme();
const { enqueueSnackbar } = useSnackbar();
const [viewerShadowed, setViewerShadowed] = useState<Viewer | undefined>(undefined);
const formRef = React.useRef<HTMLFormElement>(null);
const [magicVarOpen, setMagicVarOpen] = useState(false);
const [wopiCached, setWopiCached] = useState("");
const withDefaultIcon = useMemo(() => {
return ViewerIDWithDefaultIcons.includes(viewer.id);
}, [viewer.id]);
useEffect(() => {
setViewerShadowed({ ...viewer });
setWopiCached("");
}, [viewer, setWopiCached, setViewerShadowed]);
const onSubmit = useCallback(() => {
if (formRef.current && !formRef.current.checkValidity()) {
formRef.current.reportValidity();
return;
}
let changed = viewerShadowed;
if (!viewerShadowed || !changed) {
return;
}
if (wopiCached != "") {
try {
const parsed = JSON.parse(wopiCached);
changed = { ...viewerShadowed, wopi_actions: parsed };
setViewerShadowed({ ...changed });
} catch (e) {
enqueueSnackbar({
message: t("settings.invalidWopiActionMapping"),
variant: "warning",
action: DefaultCloseAction,
});
return;
}
}
onChange(changed);
onClose();
}, [viewerShadowed, wopiCached, formRef]);
const openMagicVar = useCallback((e: React.MouseEvent<HTMLElement>) => {
setMagicVarOpen(true);
e.stopPropagation();
e.preventDefault();
}, []);
if (!viewerShadowed) {
return null;
}
return (
<DraggableDialog
title={t("settings.editViewerTitle", {
name: t(viewer.display_name, { ns: "application" }),
})}
showActions
showCancel
onAccept={onSubmit}
dialogProps={{
fullWidth: true,
maxWidth: "lg",
open,
onClose,
}}
>
<DialogContent>
<MagicVarDialog open={magicVarOpen} vars={magicVars} onClose={() => setMagicVarOpen(false)} />
<form ref={formRef}>
<Grid spacing={2} container>
<SettingForm noContainer lgWidth={6} title={t("settings.iconUrl")}>
<DenseFilledTextField
fullWidth
required={!withDefaultIcon}
value={viewerShadowed.icon}
onChange={(e) => {
setViewerShadowed((v) => ({
...(v as Viewer),
icon: e.target.value,
}));
}}
/>
{withDefaultIcon && <NoMarginHelperText>{t("settings.builtInIconUrlDes")}</NoMarginHelperText>}
</SettingForm>
<SettingForm noContainer lgWidth={6} title={t("settings.displayName")}>
<DenseFilledTextField
fullWidth
required
value={viewerShadowed.display_name}
onChange={(e) => {
setViewerShadowed((v) => ({
...(v as Viewer),
display_name: e.target.value,
}));
}}
/>
<NoMarginHelperText>{t("settings.displayNameDes")}</NoMarginHelperText>
</SettingForm>
<SettingForm noContainer lgWidth={6} title={t("settings.exts")}>
<DenseFilledTextField
fullWidth
multiline
required
value={viewerShadowed.exts.join()}
onChange={(e) =>
setViewerShadowed((v) => ({
...(v as Viewer),
exts: e.target.value.split(",").map((ext) => ext.trim()),
}))
}
/>
</SettingForm>
{viewer.type == ViewerType.custom && (
<SettingForm noContainer lgWidth={6} title={t("settings.viewerUrl")}>
<DenseFilledTextField
fullWidth
required
value={viewerShadowed.url}
onChange={(e) =>
setViewerShadowed((v) => ({
...(v as Viewer),
url: e.target.value,
}))
}
/>
<NoMarginHelperText>
<Trans
i18nKey={"settings.viewerUrlDes"}
ns={"dashboard"}
components={[<Link onClick={openMagicVar} href={"#"} />]}
/>
</NoMarginHelperText>
</SettingForm>
)}
<SettingForm noContainer lgWidth={6} title={t("settings.maxSize")}>
<FormControl fullWidth>
<SizeInput
variant={"outlined"}
required
allowZero={true}
value={viewerShadowed.max_size ?? 0}
onChange={(e) =>
setViewerShadowed((v) => ({
...(v as Viewer),
max_size: e ? e : undefined,
}))
}
/>
<NoMarginHelperText>{t("settings.maxSizeDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
<SettingForm noContainer lgWidth={6} title={t("settings.viewerPlatform")}>
<FormControl fullWidth>
<DenseSelect
value={viewerShadowed.platform || ViewerPlatform.all}
onChange={(e) =>
setViewerShadowed((v) => ({
...(v as Viewer),
platform: e.target.value as ViewerPlatform,
}))
}
>
<SquareMenuItem value="pc">
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.viewerPlatformPC")}
</ListItemText>
</SquareMenuItem>
<SquareMenuItem value="mobile">
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.viewerPlatformMobile")}
</ListItemText>
</SquareMenuItem>
<SquareMenuItem value="all">
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.viewerPlatformAll")}
</ListItemText>
</SquareMenuItem>
</DenseSelect>
<NoMarginHelperText>{t("settings.viewerPlatformDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
<SettingForm noContainer lgWidth={6}>
<FormControlLabel
control={
<Switch
checked={isTrueVal(viewerShadowed.props?.openInNew ?? "")}
onChange={(e) =>
setViewerShadowed((v) => ({
...(v as Viewer),
props: {
...(v?.props ?? {}),
openInNew: e.target.checked.toString(),
},
}))
}
/>
}
label={t("settings.openInNew")}
/>
<NoMarginHelperText>{t("settings.openInNewDes")}</NoMarginHelperText>
</SettingForm>
{viewer.id == builtInViewers.drawio && (
<SettingForm noContainer title={t("settings.drawioHost")} lgWidth={6}>
<DenseFilledTextField
fullWidth
required
value={viewerShadowed.props?.host ?? ""}
onChange={(e) =>
setViewerShadowed((v) => ({
...(v as Viewer),
props: {
...(v?.props ?? {}),
host: e.target.value,
},
}))
}
/>
<NoMarginHelperText>{t("settings.drawioHostDes")}</NoMarginHelperText>
</SettingForm>
)}
{viewer.type == ViewerType.wopi && (
<SettingForm noContainer title={t("settings.woapiActionMapping")} lgWidth={12}>
<Suspense fallback={<CircularProgress />}>
<MonacoEditor
theme={theme.palette.mode === "dark" ? "vs-dark" : "vs"}
value={wopiCached == "" ? JSON.stringify(viewerShadowed.wopi_actions, null, 4) : wopiCached}
height={"300px"}
minHeight={"300px"}
language={"json"}
onChange={(e) => setWopiCached(e as string)}
/>
</Suspense>
</SettingForm>
)}
<SettingForm noContainer title={t("settings.newFileAction")} lgWidth={12}>
{viewerShadowed?.templates && viewerShadowed.templates.length > 0 && (
<DndProvider backend={HTML5Backend}>
<TableContainer sx={{ mt: 1, maxHeight: 440 }} component={StyledTableContainerPaper}>
<Table
stickyHeader
sx={{
width: "100%",
maxHeight: 300,
tableLayout: "fixed",
}}
size="small"
>
<TableHead>
<TableRow>
<NoWrapTableCell width={100}>{t("settings.ext")}</NoWrapTableCell>
<NoWrapTableCell width={200}>{t("settings.displayName")}</NoWrapTableCell>
<NoWrapTableCell width={100}>{t("settings.actions")}</NoWrapTableCell>
</TableRow>
</TableHead>
<TableBody>
{viewerShadowed.templates?.map((template, i) => (
<DraggableTemplateRow
key={i}
t={template}
i={i}
moveRow={(from, to) => {
if (from === to || to < 0 || to >= (viewerShadowed.templates?.length ?? 0)) return;
setViewerShadowed((v) => {
const arr = [...(v?.templates ?? [])];
const [moved] = arr.splice(from, 1);
arr.splice(to, 0, moved);
return { ...(v as Viewer), templates: arr };
});
}}
onExtChange={(e) => {
const newExt = e.target.value as string;
setViewerShadowed((v) => ({
...(v as Viewer),
templates: (v?.templates ?? []).map((template, index) =>
index == i ? { ...template, ext: newExt } : template,
),
}));
}}
onNameChange={(e) => {
setViewerShadowed((v) => ({
...(v as Viewer),
templates: (v?.templates ?? []).map((template, index) =>
index == i ? { ...template, display_name: e.target.value } : template,
),
}));
}}
onDelete={() => {
setViewerShadowed((v) => ({
...(v as Viewer),
templates: (v?.templates ?? []).filter((_, index) => index != i),
}));
}}
isFirst={i === 0}
isLast={i === (viewerShadowed.templates?.length ?? 0) - 1}
extList={viewerShadowed.exts}
template={template}
/>
))}
</TableBody>
</Table>
</TableContainer>
</DndProvider>
)}
<SecondaryButton
sx={{ mt: 1 }}
variant={"contained"}
startIcon={<Add />}
onClick={() =>
setViewerShadowed((v) => ({
...(v as Viewer),
templates: [...(v?.templates ?? []), { ext: viewerShadowed.exts?.[0] ?? "", display_name: "" }],
}))
}
>
{t("settings.addNewFileAction")}
</SecondaryButton>
<NoMarginHelperText>{t("settings.newFileActionDes")}</NoMarginHelperText>
</SettingForm>
</Grid>
</form>
</DialogContent>
</DraggableDialog>
);
};
export default FileViewerEditDialog;

View File

@@ -0,0 +1,343 @@
import { ExpandMoreRounded } from "@mui/icons-material";
import {
AccordionDetails,
Box,
Link,
ListItemIcon,
Menu,
Table,
TableBody,
TableContainer,
TableHead,
TableRow,
Typography,
} from "@mui/material";
import { bindMenu, bindTrigger, usePopupState } from "material-ui-popup-state/hooks";
import * as React from "react";
import { memo, useCallback, useMemo, useState } from "react";
import { DndProvider, useDrag, useDrop } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { useTranslation } from "react-i18next";
import { Viewer, ViewerGroup, ViewerType } from "../../../../api/explorer.ts";
import { uuidv4 } from "../../../../util";
import { NoWrapTableCell, SecondaryButton } from "../../../Common/StyledComponents.tsx";
import { SquareMenuItem } from "../../../FileManager/ContextMenu/ContextMenu.tsx";
import Add from "../../../Icons/Add.tsx";
import DesktopFlow from "../../../Icons/DesktopFlow.tsx";
import DocumentDataLink from "../../../Icons/DocumentDataLink.tsx";
import { AccordionSummary, StyledAccordion } from "../../Settings/UserSession/SSOSettings.tsx";
import FileViewerEditDialog from "./FileViewerEditDialog.tsx";
import FileViewerRow from "./FileViewerRow.tsx";
import ImportWopiDialog from "./ImportWopiDialog.tsx";
interface ViewerGroupProps {
group: ViewerGroup;
index: number;
onDelete: (e: React.MouseEvent<HTMLElement>) => void;
onGroupChange: (g: ViewerGroup) => void;
dndType: string;
}
const DND_TYPE = "viewer-row";
const DraggableViewerRow = memo(function DraggableViewerRow({
viewer,
index,
moveRow,
onChange,
onDelete,
onMoveUp,
onMoveDown,
isLast,
isFirst,
dndType,
}: any) {
const ref = React.useRef<HTMLTableRowElement>(null);
const [, drop] = useDrop({
accept: dndType,
hover(item: any, monitor) {
if (!ref.current) return;
const dragIndex = item.index;
const hoverIndex = index;
if (dragIndex === hoverIndex) return;
const hoverBoundingRect = ref.current.getBoundingClientRect();
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
const clientOffset = monitor.getClientOffset();
if (!clientOffset) return;
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) return;
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) return;
moveRow(dragIndex, hoverIndex);
item.index = hoverIndex;
},
});
const [{ isDragging }, drag] = useDrag({
type: dndType,
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
drag(drop(ref));
return (
<FileViewerRow
ref={ref}
viewer={viewer}
onChange={onChange}
onDelete={onDelete}
onMoveUp={onMoveUp}
onMoveDown={onMoveDown}
isLast={isLast}
isFirst={isFirst}
style={{ opacity: isDragging ? 0.5 : 1, cursor: "move" }}
/>
);
});
const ViewerGroupRow = memo(({ group, index, onDelete, onGroupChange, dndType }: ViewerGroupProps) => {
const { t } = useTranslation("dashboard");
const onViewerChange = useMemo(() => {
return group.viewers.map((_, index) => (vChanged: Viewer) => {
onGroupChange({
viewers: group.viewers.map((v, i) => (i == index ? vChanged : v)),
});
});
}, [group.viewers]);
const onViewerDeleted = useMemo(() => {
return group.viewers.map((_, index) => (e: React.MouseEvent<HTMLElement>) => {
onGroupChange({
viewers: group.viewers.filter((_, i) => i != index),
});
e.preventDefault();
e.stopPropagation();
});
}, [group.viewers]);
const [viewers, setViewers] = useState(group.viewers);
React.useEffect(() => {
setViewers(group.viewers);
}, [group.viewers]);
const moveRow = useCallback(
(from: number, to: number) => {
if (from === to) return;
const updated = [...viewers];
const [moved] = updated.splice(from, 1);
updated.splice(to, 0, moved);
setViewers(updated);
onGroupChange({ viewers: updated });
},
[viewers, onGroupChange],
);
const handleMoveUp = (idx: number) => {
if (idx <= 0) return;
moveRow(idx, idx - 1);
};
const handleMoveDown = (idx: number) => {
if (idx >= viewers.length - 1) return;
moveRow(idx, idx + 1);
};
return (
<StyledAccordion
defaultExpanded={index == 0}
disableGutters
slotProps={{
transition: {
unmountOnExit: true,
},
}}
>
<AccordionSummary expandIcon={<ExpandMoreRounded />}>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
width: "100%",
alignItems: "center",
}}
>
<Typography>{t("settings.viewerGroupTitle", { index: index + 1 })}</Typography>
{index > 0 && (
<Link href={"#"} onClick={onDelete}>
{t("policy.delete")}
</Link>
)}
</Box>
</AccordionSummary>
<AccordionDetails sx={{ display: "block" }}>
<DndProvider backend={HTML5Backend}>
<TableContainer sx={{ mt: 1, maxHeight: 440 }}>
<Table stickyHeader sx={{ width: "100%", tableLayout: "fixed" }} size="small">
<TableHead>
<TableRow>
<NoWrapTableCell width={64}>{t("settings.icon")}</NoWrapTableCell>
<NoWrapTableCell width={100}>{t("settings.viewerType")}</NoWrapTableCell>
<NoWrapTableCell width={200}>{t("settings.displayName")}</NoWrapTableCell>
<NoWrapTableCell width={250}>{t("settings.exts")}</NoWrapTableCell>
<NoWrapTableCell width={150}>{t("settings.viewerPlatform")}</NoWrapTableCell>
<NoWrapTableCell width={100}>{t("settings.newFileAction")}</NoWrapTableCell>
<NoWrapTableCell width={64}>{t("settings.viewerEnabled")}</NoWrapTableCell>
<NoWrapTableCell width={100}>{t("settings.actions")}</NoWrapTableCell>
<NoWrapTableCell width={100}></NoWrapTableCell>
</TableRow>
</TableHead>
<TableBody>
{viewers.map((viewer, idx) => (
<DraggableViewerRow
key={viewer.id}
viewer={viewer}
index={idx}
moveRow={moveRow}
onChange={onViewerChange[idx]}
onDelete={onViewerDeleted[idx]}
onMoveUp={() => handleMoveUp(idx)}
onMoveDown={() => handleMoveDown(idx)}
isFirst={idx === 0}
isLast={idx === viewers.length - 1}
dndType={dndType}
/>
))}
</TableBody>
</Table>
</TableContainer>
</DndProvider>
</AccordionDetails>
</StyledAccordion>
);
});
export interface FileViewerListProps {
config: string;
onChange: (value: string) => void;
}
const FileViewerList = memo(({ config, onChange }: FileViewerListProps) => {
const { t } = useTranslation("dashboard");
const addNewPopupState = usePopupState({
variant: "popover",
popupId: "addNewViewer",
});
const [createNewOpen, setCreateNewOpen] = useState(false);
const [newViewer, setNewViewer] = useState<Viewer | undefined>(undefined);
const [importOpen, setImportOpen] = useState(false);
const configParsed = useMemo((): ViewerGroup[] => JSON.parse(config), [config]);
const onNewViewerChange = useCallback(
(v: Viewer) => {
setNewViewer(v);
const newViewerSetting = [...configParsed];
newViewerSetting[0].viewers.push(v);
onChange(JSON.stringify(newViewerSetting));
},
[configParsed],
);
const onGroupDelete = useMemo(() => {
return configParsed.map((_, index) => (e: React.MouseEvent<HTMLElement>) => {
onChange(JSON.stringify([...configParsed].filter((_, i) => i != index)));
e.preventDefault();
e.stopPropagation();
});
}, [configParsed]);
const onGroupChange = useMemo(() => {
return configParsed.map((_, index) => (g: ViewerGroup) => {
onChange(JSON.stringify([...configParsed].map((item, i) => (i == index ? g : item))));
});
}, [configParsed]);
const { onClose, ...menuProps } = bindMenu(addNewPopupState);
const onCreateNewClosed = useCallback(() => {
setCreateNewOpen(false);
}, []);
const openCreateNew = useCallback(() => {
setNewViewer({
id: uuidv4(),
icon: "",
type: ViewerType.custom,
display_name: "",
exts: [],
});
setCreateNewOpen(true);
onClose();
}, [onClose, setNewViewer]);
const openImportNew = useCallback(() => {
setImportOpen(true);
onClose();
}, [onClose, setImportOpen]);
const onImportedNew = useCallback(
(v: ViewerGroup) => {
const newViewerSetting = [...configParsed];
newViewerSetting.push(v);
onChange(JSON.stringify(newViewerSetting));
},
[configParsed],
);
return (
<Box>
<SecondaryButton variant={"contained"} {...bindTrigger(addNewPopupState)} startIcon={<Add />} sx={{ mb: 1 }}>
{t("settings.addViewer")}
</SecondaryButton>
{configParsed?.length > 0 &&
configParsed.map((item: ViewerGroup, index) => (
<ViewerGroupRow
group={item}
index={index}
key={index}
onDelete={onGroupDelete[index]}
onGroupChange={onGroupChange[index]}
dndType={`viewer-row-${index}`}
/>
))}
<Menu
onClose={onClose}
anchorOrigin={{
vertical: "top",
horizontal: "left",
}}
transformOrigin={{
vertical: "bottom",
horizontal: "left",
}}
{...menuProps}
>
<SquareMenuItem dense onClick={openCreateNew}>
<ListItemIcon>
<DocumentDataLink />
</ListItemIcon>
{t("settings.embeddedWebpageViewer")}
</SquareMenuItem>
<SquareMenuItem dense onClick={openImportNew}>
<ListItemIcon>
<DesktopFlow />
</ListItemIcon>
{t("settings.wopiViewer")}
</SquareMenuItem>
</Menu>
{newViewer && (
<FileViewerEditDialog
viewer={newViewer}
onChange={onNewViewerChange}
open={createNewOpen}
onClose={onCreateNewClosed}
/>
)}
<ImportWopiDialog onImported={onImportedNew} onClose={() => setImportOpen(false)} open={importOpen} />
</Box>
);
});
export default FileViewerList;

View File

@@ -0,0 +1,151 @@
import * as React from "react";
import { useCallback, useState } from "react";
import { Viewer, ViewerPlatform, ViewerType } from "../../../../api/explorer.ts";
import { useTranslation } from "react-i18next";
import { IconButton, ListItemText, TableRow } from "@mui/material";
import { DenseFilledTextField, DenseSelect, NoWrapCell, StyledCheckbox } from "../../../Common/StyledComponents.tsx";
import { SquareMenuItem } from "../../../FileManager/ContextMenu/ContextMenu.tsx";
import { ViewerIcon } from "../../../FileManager/Dialogs/OpenWith.tsx";
import Dismiss from "../../../Icons/Dismiss.tsx";
import Edit from "../../../Icons/Edit.tsx";
import FileViewerEditDialog from "./FileViewerEditDialog.tsx";
import ArrowDown from "../../../Icons/ArrowDown.tsx";
export interface FileViewerRowProps {
viewer: Viewer;
onChange: (viewer: Viewer) => void;
onDelete: (e: React.MouseEvent<HTMLElement>) => void;
onMoveUp?: () => void;
onMoveDown?: () => void;
isFirst?: boolean;
isLast?: boolean;
style?: React.CSSProperties;
}
const FileViewerRow = React.memo(
React.forwardRef<HTMLTableRowElement, FileViewerRowProps>(
({ viewer, onChange, onDelete, onMoveUp, onMoveDown, isFirst, isLast, style }, ref) => {
const { t } = useTranslation("dashboard");
const [extCached, setExtCached] = useState("");
const [editOpen, setEditOpen] = useState(false);
const onClose = useCallback(() => {
setEditOpen(false);
}, [setEditOpen]);
return (
<TableRow sx={{ "&:last-child td, &:last-child th": { border: 0 } }} hover ref={ref} style={style}>
<FileViewerEditDialog viewer={viewer} onChange={onChange} open={editOpen} onClose={onClose} />
<NoWrapCell>
<ViewerIcon viewer={viewer} />
</NoWrapCell>
<NoWrapCell>{t(`settings.${viewer.type}ViewerType`)}</NoWrapCell>
<NoWrapCell>
{t(viewer.display_name, {
ns: "application",
})}
</NoWrapCell>
<NoWrapCell>
<DenseFilledTextField
fullWidth
multiline
required
value={extCached == "" ? viewer.exts.join() : extCached}
onBlur={() => {
onChange({
...viewer,
exts: extCached == "" ? viewer.exts : extCached?.split(",")?.map((ext) => ext.trim()),
});
setExtCached("");
}}
onChange={(e) => setExtCached(e.target.value)}
/>
</NoWrapCell>
<NoWrapCell>
<DenseSelect
value={viewer.platform || ViewerPlatform.all}
onChange={(e) =>
onChange({
...viewer,
platform: e.target.value as ViewerPlatform,
})
}
>
<SquareMenuItem value="pc">
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.viewerPlatformPC")}
</ListItemText>
</SquareMenuItem>
<SquareMenuItem value="mobile">
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.viewerPlatformMobile")}
</ListItemText>
</SquareMenuItem>
<SquareMenuItem value="all">
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.viewerPlatformAll")}
</ListItemText>
</SquareMenuItem>
</DenseSelect>
</NoWrapCell>
<NoWrapCell>
{viewer.templates?.length ? t("settings.nMapping", { num: viewer.templates?.length }) : t("share.none")}
</NoWrapCell>
<NoWrapCell>
<StyledCheckbox
size={"small"}
checked={!viewer.disabled}
onChange={(e) =>
onChange({
...viewer,
disabled: !e.target.checked,
})
}
/>
</NoWrapCell>
<NoWrapCell>
<IconButton size={"small"} onClick={() => setEditOpen(true)}>
<Edit fontSize={"small"} />
</IconButton>
{viewer.type != ViewerType.builtin && (
<IconButton size={"small"} onClick={onDelete}>
<Dismiss fontSize={"small"} />
</IconButton>
)}
</NoWrapCell>
<NoWrapCell>
<IconButton size="small" onClick={onMoveUp} disabled={isFirst}>
<ArrowDown
sx={{
width: "18px",
height: "18px",
transform: "rotate(180deg)",
}}
/>
</IconButton>
<IconButton size="small" onClick={onMoveDown} disabled={isLast}>
<ArrowDown
sx={{
width: "18px",
height: "18px",
}}
/>
</IconButton>
</NoWrapCell>
</TableRow>
);
},
),
);
export default FileViewerRow;

View File

@@ -0,0 +1,72 @@
import { DialogContent, Link } from "@mui/material";
import { useCallback, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { getWopiDiscovery } from "../../../../api/api.ts";
import { ViewerGroup } from "../../../../api/explorer.ts";
import { useAppDispatch } from "../../../../redux/hooks.ts";
import { DenseFilledTextField } from "../../../Common/StyledComponents.tsx";
import DraggableDialog from "../../../Dialogs/DraggableDialog.tsx";
import SettingForm from "../../../Pages/Setting/SettingForm.tsx";
import { Code } from "../../../Common/Code.tsx";
import { NoMarginHelperText } from "../../Settings/Settings.tsx";
export interface ImportWopiDialogProps {
open: boolean;
onClose: () => void;
onImported: (v: ViewerGroup) => void;
}
const ImportWopiDialog = ({ open, onClose, onImported }: ImportWopiDialogProps) => {
const { t } = useTranslation("dashboard");
const dispatch = useAppDispatch();
const [endpoint, setEndpoint] = useState("");
const [loading, setLoading] = useState(false);
const onSubmit = useCallback(() => {
setLoading(true);
dispatch(
getWopiDiscovery({
endpoint,
}),
)
.then((res) => {
onImported(res);
onClose();
})
.finally(() => {
setLoading(false);
});
}, [endpoint, onClose, onImported]);
return (
<DraggableDialog
title={t("settings.importWopi")}
showActions
showCancel
onAccept={onSubmit}
disabled={endpoint == ""}
loading={loading}
dialogProps={{
fullWidth: true,
maxWidth: "md",
open,
onClose,
}}
>
<DialogContent>
<SettingForm lgWidth={12} title={t("settings.wopiEndpoint")}>
<DenseFilledTextField value={endpoint} fullWidth onChange={(e) => setEndpoint(e.target.value)} />
<NoMarginHelperText>
<Trans
ns="dashboard"
i18nKey="settings.wopiDes"
components={[<Code />, <Link href="https://docs.cloudreve.org/usage/wopi" target="_blank" />]}
/>
</NoMarginHelperText>
</SettingForm>
</DialogContent>
</DraggableDialog>
);
};
export default ImportWopiDialog;

View File

@@ -0,0 +1,33 @@
import { Box, Stack } from "@mui/material";
import { useSnackbar } from "notistack";
import { useCallback, useContext, useState } from "react";
import { useTranslation } from "react-i18next";
import { useAppDispatch } from "../../../../redux/hooks";
import { SettingContext } from "../../Settings/SettingWrapper";
import FileViewerList from "./FileViewerList";
const ViewerSetting = () => {
const { t } = useTranslation("dashboard");
const { formRef, setSettings, values } = useContext(SettingContext);
const [loading, setLoading] = useState(false);
const dispatch = useAppDispatch();
const { enqueueSnackbar } = useSnackbar();
const viewerOnChange = useCallback(
(s: string) =>
setSettings({
file_viewers: s,
}),
[],
);
return (
<Box component={"form"} ref={formRef} onSubmit={(e) => e.preventDefault()}>
<Stack spacing={5}>
<FileViewerList config={values.file_viewers} onChange={viewerOnChange} />
</Stack>
</Box>
);
};
export default ViewerSetting;