123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 |
- import { ReactNode, useEffect, useRef, useState } from "react";
- import { useLockFn } from "ahooks";
- import { useTranslation } from "react-i18next";
- import {
- Button,
- ButtonGroup,
- Dialog,
- DialogActions,
- DialogContent,
- DialogTitle,
- IconButton,
- } from "@mui/material";
- import {
- FormatPaintRounded,
- OpenInFullRounded,
- CloseFullscreenRounded,
- } from "@mui/icons-material";
- import { useThemeMode } from "@/services/states";
- import { Notice } from "@/components/base";
- import { nanoid } from "nanoid";
- import { appWindow } from "@tauri-apps/api/window";
- import getSystem from "@/utils/get-system";
- import debounce from "@/utils/debounce";
- import * as monaco from "monaco-editor";
- import MonacoEditor from "react-monaco-editor";
- import { configureMonacoYaml } from "monaco-yaml";
- import { type JSONSchema7 } from "json-schema";
- import metaSchema from "meta-json-schema/schemas/meta-json-schema.json";
- import mergeSchema from "meta-json-schema/schemas/clash-verge-merge-json-schema.json";
- import pac from "types-pac/pac.d.ts?raw";
- type Language = "yaml" | "javascript" | "css";
- type Schema<T extends Language> = LanguageSchemaMap[T];
- interface LanguageSchemaMap {
- yaml: "clash" | "merge";
- javascript: never;
- css: never;
- }
- interface Props<T extends Language> {
- open: boolean;
- title?: string | ReactNode;
- initialData: Promise<string>;
- readOnly?: boolean;
- language: T;
- schema?: Schema<T>;
- onChange?: (prev?: string, curr?: string) => void;
- onSave?: (prev?: string, curr?: string) => void;
- onClose: () => void;
- }
- let initialized = false;
- const monacoInitialization = () => {
- if (initialized) return;
- // configure yaml worker
- configureMonacoYaml(monaco, {
- validate: true,
- enableSchemaRequest: true,
- schemas: [
- {
- uri: "http://example.com/meta-json-schema.json",
- fileMatch: ["**/*.clash.yaml"],
- // @ts-ignore
- schema: metaSchema as JSONSchema7,
- },
- {
- uri: "http://example.com/clash-verge-merge-json-schema.json",
- fileMatch: ["**/*.merge.yaml"],
- // @ts-ignore
- schema: mergeSchema as JSONSchema7,
- },
- ],
- });
- // configure PAC definition
- monaco.languages.typescript.javascriptDefaults.addExtraLib(pac, "pac.d.ts");
- initialized = true;
- };
- export const EditorViewer = <T extends Language>(props: Props<T>) => {
- const { t } = useTranslation();
- const themeMode = useThemeMode();
- const [isMaximized, setIsMaximized] = useState(false);
- const {
- open = false,
- title = t("Edit File"),
- initialData = Promise.resolve(""),
- readOnly = false,
- language = "yaml",
- schema,
- onChange,
- onSave,
- onClose,
- } = props;
- const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
- const prevData = useRef<string | undefined>("");
- const currData = useRef<string | undefined>("");
- const editorWillMount = () => {
- monacoInitialization(); // initialize monaco
- };
- const editorDidMount = async (
- editor: monaco.editor.IStandaloneCodeEditor
- ) => {
- editorRef.current = editor;
- // retrieve initial data
- await initialData.then((data) => {
- prevData.current = data;
- currData.current = data;
- // create and set model
- const uri = monaco.Uri.parse(`${nanoid()}.${schema}.${language}`);
- const model = monaco.editor.createModel(data, language, uri);
- editorRef.current?.setModel(model);
- });
- };
- const handleChange = useLockFn(async (value: string | undefined) => {
- try {
- currData.current = value;
- onChange?.(prevData.current, currData.current);
- } catch (err: any) {
- Notice.error(err.message || err.toString());
- }
- });
- const handleSave = useLockFn(async () => {
- try {
- !readOnly && onSave?.(prevData.current, currData.current);
- onClose();
- } catch (err: any) {
- Notice.error(err.message || err.toString());
- }
- });
- const handleClose = useLockFn(async () => {
- try {
- onClose();
- } catch (err: any) {
- Notice.error(err.message || err.toString());
- }
- });
- const editorResize = debounce(() => {
- editorRef.current?.layout();
- setTimeout(() => editorRef.current?.layout(), 500);
- }, 100);
- useEffect(() => {
- const onResized = debounce(() => {
- editorResize();
- appWindow.isMaximized().then((maximized) => {
- setIsMaximized(() => maximized);
- });
- }, 100);
- const unlistenResized = appWindow.onResized(onResized);
- return () => {
- unlistenResized.then((fn) => fn());
- editorRef.current?.dispose();
- editorRef.current = undefined;
- };
- }, []);
- return (
- <Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
- <DialogTitle>{title}</DialogTitle>
- <DialogContent
- sx={{
- width: "auto",
- height: "calc(100vh - 185px)",
- overflow: "hidden",
- }}
- >
- <MonacoEditor
- language={language}
- theme={themeMode === "light" ? "vs" : "vs-dark"}
- options={{
- tabSize: ["yaml", "javascript", "css"].includes(language) ? 2 : 4, // 根据语言类型设置缩进大小
- minimap: {
- enabled: document.documentElement.clientWidth >= 1500, // 超过一定宽度显示minimap滚动条
- },
- mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例
- readOnly: readOnly, // 只读模式
- readOnlyMessage: { value: t("ReadOnlyMessage") }, // 只读模式尝试编辑时的提示信息
- renderValidationDecorations: "on", // 只读模式下显示校验信息
- quickSuggestions: {
- strings: true, // 字符串类型的建议
- comments: true, // 注释类型的建议
- other: true, // 其他类型的建议
- },
- padding: {
- top: 33, // 顶部padding防止遮挡snippets
- },
- fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
- getSystem() === "windows" ? ", twemoji mozilla" : ""
- }`,
- fontLigatures: true, // 连字符
- smoothScrolling: true, // 平滑滚动
- }}
- editorWillMount={editorWillMount}
- editorDidMount={editorDidMount}
- onChange={handleChange}
- />
- <ButtonGroup
- variant="contained"
- sx={{ position: "absolute", left: "14px", bottom: "8px" }}
- >
- <IconButton
- size="medium"
- color="inherit"
- sx={{ display: readOnly ? "none" : "" }}
- title={t("Format document")}
- onClick={() =>
- editorRef.current
- ?.getAction("editor.action.formatDocument")
- ?.run()
- }
- >
- <FormatPaintRounded fontSize="inherit" />
- </IconButton>
- <IconButton
- size="medium"
- color="inherit"
- title={t(isMaximized ? "Minimize" : "Maximize")}
- onClick={() => appWindow.toggleMaximize().then(editorResize)}
- >
- {isMaximized ? <CloseFullscreenRounded /> : <OpenInFullRounded />}
- </IconButton>
- </ButtonGroup>
- </DialogContent>
- <DialogActions>
- <Button onClick={handleClose} variant="outlined">
- {t(readOnly ? "Close" : "Cancel")}
- </Button>
- {!readOnly && (
- <Button onClick={handleSave} variant="contained">
- {t("Save")}
- </Button>
- )}
- </DialogActions>
- </Dialog>
- );
- };
|