feat: supports edit profile file

This commit is contained in:
GyDi 2022-03-27 00:58:17 +08:00
parent 9d44668d5f
commit f31349eaa0
No known key found for this signature in database
GPG Key ID: 1C95E0D3467B3084
12 changed files with 259 additions and 38 deletions

View File

@ -25,6 +25,7 @@
"axios": "^0.26.0",
"dayjs": "^1.10.8",
"i18next": "^21.6.14",
"monaco-editor": "^0.33.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-i18next": "^11.15.6",
@ -49,7 +50,8 @@
"pretty-quick": "^3.1.3",
"sass": "^1.49.7",
"typescript": "^4.5.5",
"vite": "^2.8.6"
"vite": "^2.8.6",
"vite-plugin-monaco-editor": "^1.0.10"
},
"prettier": {
"tabWidth": 2,

View File

@ -212,6 +212,35 @@ pub fn view_profile(index: String, profiles_state: State<'_, ProfilesState>) ->
wrap_err!(open::that(path))
}
/// read the profile item file data
#[tauri::command]
pub fn read_profile_file(
index: String,
profiles_state: State<'_, ProfilesState>,
) -> Result<String, String> {
let profiles = profiles_state.0.lock().unwrap();
let item = wrap_err!(profiles.get_item(&index))?;
let data = wrap_err!(item.read_file())?;
Ok(data)
}
/// save the profile item file data
#[tauri::command]
pub fn save_profile_file(
index: String,
file_data: Option<String>,
profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> {
if file_data.is_none() {
return Ok(());
}
let profiles = profiles_state.0.lock().unwrap();
let item = wrap_err!(profiles.get_item(&index))?;
wrap_err!(item.save_file(file_data.unwrap()))
}
/// restart the sidecar
#[tauri::command]
pub fn restart_sidecar(

View File

@ -279,6 +279,28 @@ impl PrfItem {
file_data: Some(tmpl::ITEM_SCRIPT.into()),
})
}
/// get the file data
pub fn read_file(&self) -> Result<String> {
if self.file.is_none() {
bail!("could not find the file");
}
let file = self.file.clone().unwrap();
let path = dirs::app_profiles_dir().join(file);
fs::read_to_string(path).context("failed to read the file")
}
/// save the file data
pub fn save_file(&self, data: String) -> Result<()> {
if self.file.is_none() {
bail!("could not find the file");
}
let file = self.file.clone().unwrap();
let path = dirs::app_profiles_dir().join(file);
fs::write(path, data.as_bytes()).context("failed to save the file")
}
}
///

View File

@ -118,7 +118,9 @@ fn main() -> std::io::Result<()> {
cmds::get_profiles,
cmds::sync_profiles,
cmds::enhance_profiles,
cmds::change_profile_chain
cmds::change_profile_chain,
cmds::read_profile_file,
cmds::save_profile_file
]);
#[cfg(target_os = "macos")]

View File

@ -0,0 +1,97 @@
import useSWR from "swr";
import { useEffect, useRef } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
} from "@mui/material";
import {
getVergeConfig,
readProfileFile,
saveProfileFile,
} from "../../services/cmds";
import Notice from "../base/base-notice";
import "monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js";
import "monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution.js";
import "monaco-editor/esm/vs/editor/contrib/folding/browser/folding.js";
import { editor } from "monaco-editor/esm/vs/editor/editor.api";
interface Props {
uid: string;
open: boolean;
mode: "yaml" | "javascript";
onClose: () => void;
onChange?: () => void;
}
const FileEditor = (props: Props) => {
const { uid, open, mode, onClose, onChange } = props;
const { t } = useTranslation();
const editorRef = useRef<any>();
const instanceRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const { data: vergeConfig } = useSWR("getVergeConfig", getVergeConfig);
const { theme_mode } = vergeConfig ?? {};
useEffect(() => {
if (!open) return;
readProfileFile(uid).then((data) => {
const dom = editorRef.current;
if (!dom) return;
if (instanceRef.current) instanceRef.current.dispose();
instanceRef.current = editor.create(editorRef.current, {
value: data,
language: mode,
theme: theme_mode === "light" ? "vs" : "vs-dark",
minimap: { enabled: false },
});
});
return () => {
if (instanceRef.current) {
instanceRef.current.dispose();
instanceRef.current = null;
}
};
}, [open]);
const onSave = useLockFn(async () => {
const value = instanceRef.current?.getValue();
if (value == null) return;
try {
await saveProfileFile(uid, value);
onChange?.();
} catch (err: any) {
Notice.error(err.message || err.toString());
}
});
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>{t("Edit File")}</DialogTitle>
<DialogContent sx={{ width: 520, pb: 1 }}>
<div style={{ width: "100%", height: "420px" }} ref={editorRef} />
</DialogContent>
<DialogActions>
<Button onClick={onClose}>{t("Cancel")}</Button>
<Button onClick={onSave} variant="contained">
{t("Save")}
</Button>
</DialogActions>
</Dialog>
);
};
export default FileEditor;

View File

@ -21,6 +21,7 @@ import { atomLoadingCache } from "../../services/states";
import { updateProfile, deleteProfile, viewProfile } from "../../services/cmds";
import parseTraffic from "../../utils/parse-traffic";
import ProfileEdit from "./profile-edit";
import FileEditor from "./file-editor";
import Notice from "../base/base-notice";
const Wrapper = styled(Box)(({ theme }) => ({
@ -54,7 +55,7 @@ const ProfileItem = (props: Props) => {
const [position, setPosition] = useState({ left: 0, top: 0 });
const [loadingCache, setLoadingCache] = useRecoilState(atomLoadingCache);
const { name = "Profile", extra, updated = 0 } = itemData;
const { uid, name = "Profile", extra, updated = 0 } = itemData;
const { upload = 0, download = 0, total = 0 } = extra ?? {};
const from = parseUrl(itemData.url);
const expire = parseExpire(extra?.expire);
@ -70,18 +71,16 @@ const ProfileItem = (props: Props) => {
const loading = loadingCache[itemData.uid] ?? false;
const [editOpen, setEditOpen] = useState(false);
const onEdit = () => {
const [fileOpen, setFileOpen] = useState(false);
const onEditInfo = () => {
setAnchorEl(null);
setEditOpen(true);
};
const onView = async () => {
const onEditFile = () => {
setAnchorEl(null);
try {
await viewProfile(itemData.uid);
} catch (err: any) {
Notice.error(err?.message || err.toString());
}
setFileOpen(true);
};
const onForceSelect = () => {
@ -89,6 +88,15 @@ const ProfileItem = (props: Props) => {
onSelect(true);
};
const onOpenFile = useLockFn(async () => {
setAnchorEl(null);
try {
await viewProfile(itemData.uid);
} catch (err: any) {
Notice.error(err?.message || err.toString());
}
});
const onUpdate = useLockFn(async (withProxy: boolean) => {
setAnchorEl(null);
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: true }));
@ -122,16 +130,18 @@ const ProfileItem = (props: Props) => {
const urlModeMenu = [
{ label: "Select", handler: onForceSelect },
{ label: "Edit", handler: onEdit },
{ label: "File", handler: onView },
{ label: "Edit Info", handler: onEditInfo },
{ label: "Edit File", handler: onEditFile },
{ label: "Open File", handler: onOpenFile },
{ label: "Update", handler: () => onUpdate(false) },
{ label: "Update(Proxy)", handler: () => onUpdate(true) },
{ label: "Delete", handler: onDelete },
];
const fileModeMenu = [
{ label: "Select", handler: onForceSelect },
{ label: "Edit", handler: onEdit },
{ label: "File", handler: onView },
{ label: "Edit Info", handler: onEditInfo },
{ label: "Edit File", handler: onEditFile },
{ label: "Open File", handler: onOpenFile },
{ label: "Delete", handler: onDelete },
];
@ -256,6 +266,7 @@ const ProfileItem = (props: Props) => {
onClose={() => setAnchorEl(null)}
anchorPosition={position}
anchorReference="anchorPosition"
transitionDuration={225}
onContextMenu={(e) => {
setAnchorEl(null);
e.preventDefault();
@ -279,6 +290,15 @@ const ProfileItem = (props: Props) => {
onClose={() => setEditOpen(false)}
/>
)}
{fileOpen && (
<FileEditor
uid={uid}
open={fileOpen}
mode="yaml"
onClose={() => setFileOpen(false)}
/>
)}
</>
);
};

View File

@ -1,6 +1,7 @@
import dayjs from "dayjs";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
import {
alpha,
Box,
@ -12,9 +13,10 @@ import {
} from "@mui/material";
import { CmdType } from "../../services/types";
import { viewProfile } from "../../services/cmds";
import ProfileEdit from "./profile-edit";
import Notice from "../base/base-notice";
import enhance from "../../services/enhance";
import ProfileEdit from "./profile-edit";
import FileEditor from "./file-editor";
import Notice from "../base/base-notice";
const Wrapper = styled(Box)(({ theme }) => ({
width: "100%",
@ -57,6 +59,7 @@ const ProfileMore = (props: Props) => {
const [anchorEl, setAnchorEl] = useState<any>(null);
const [position, setPosition] = useState({ left: 0, top: 0 });
const [editOpen, setEditOpen] = useState(false);
const [fileOpen, setFileOpen] = useState(false);
const [status, setStatus] = useState(enhance.status(uid));
// unlisten when unmount
@ -65,40 +68,47 @@ const ProfileMore = (props: Props) => {
// error during enhanced mode
const hasError = selected && status?.status === "error";
const onEdit = () => {
const onEditInfo = () => {
setAnchorEl(null);
setEditOpen(true);
};
const onView = async () => {
const onEditFile = () => {
setAnchorEl(null);
setFileOpen(true);
};
const onOpenFile = useLockFn(async () => {
setAnchorEl(null);
try {
await viewProfile(itemData.uid);
} catch (err: any) {
Notice.error(err?.message || err.toString());
}
};
});
const closeWrapper = (fn: () => void) => () => {
const fnWrapper = (fn: () => void) => () => {
setAnchorEl(null);
return fn();
};
const enableMenu = [
{ label: "Disable", handler: closeWrapper(onDisable) },
{ label: "Refresh", handler: closeWrapper(onEnhance) },
{ label: "Edit", handler: onEdit },
{ label: "File", handler: onView },
{ label: "To Top", show: !hasError, handler: closeWrapper(onMoveTop) },
{ label: "To End", show: !hasError, handler: closeWrapper(onMoveEnd) },
{ label: "Delete", handler: closeWrapper(onDelete) },
{ label: "Disable", handler: fnWrapper(onDisable) },
{ label: "Refresh", handler: fnWrapper(onEnhance) },
{ label: "Edit Info", handler: onEditInfo },
{ label: "Edit File", handler: onEditFile },
{ label: "Open File", handler: onOpenFile },
{ label: "To Top", show: !hasError, handler: fnWrapper(onMoveTop) },
{ label: "To End", show: !hasError, handler: fnWrapper(onMoveEnd) },
{ label: "Delete", handler: fnWrapper(onDelete) },
];
const disableMenu = [
{ label: "Enable", handler: closeWrapper(onEnable) },
{ label: "Edit", handler: onEdit },
{ label: "File", handler: onView },
{ label: "Delete", handler: closeWrapper(onDelete) },
{ label: "Enable", handler: fnWrapper(onEnable) },
{ label: "Edit Info", handler: onEditInfo },
{ label: "Edit File", handler: onEditFile },
{ label: "Open File", handler: onOpenFile },
{ label: "Delete", handler: fnWrapper(onDelete) },
];
const boxStyle = {
@ -208,6 +218,7 @@ const ProfileMore = (props: Props) => {
onClose={() => setAnchorEl(null)}
anchorPosition={position}
anchorReference="anchorPosition"
transitionDuration={225}
onContextMenu={(e) => {
setAnchorEl(null);
e.preventDefault();
@ -233,6 +244,15 @@ const ProfileMore = (props: Props) => {
onClose={() => setEditOpen(false)}
/>
)}
{fileOpen && (
<FileEditor
uid={uid}
open={fileOpen}
mode={type === "merge" ? "yaml" : "javascript"}
onClose={() => setFileOpen(false)}
/>
)}
</>
);
};

View File

@ -19,8 +19,9 @@
"New": "New",
"Close All": "Close All",
"Select": "Select",
"Edit": "Edit",
"File": "File",
"Edit Info": "Edit Info",
"Edit File": "Edit File",
"Open File": "Open File",
"Update": "Update",
"Update(Proxy)": "Update(Proxy)",
"Delete": "Delete",
@ -41,6 +42,7 @@
"Clash core": "Clash core",
"Tun Mode": "Tun Mode",
"Auto Launch": "Auto Launch",
"Silent Start": "Silent Start",
"System Proxy": "System Proxy",
"Proxy Guard": "Proxy Guard",
"Proxy Bypass": "Proxy Bypass",
@ -50,5 +52,8 @@
"Language": "Language",
"Open App Dir": "Open App Dir",
"Open Logs Dir": "Open Logs Dir",
"Version": "Version"
"Version": "Version",
"Save": "Save",
"Cancel": "Cancel"
}

View File

@ -19,8 +19,9 @@
"New": "新建",
"Close All": "关闭全部",
"Select": "使用",
"Edit": "编辑信息",
"File": "打开文件",
"Edit Info": "编辑信息",
"Edit File": "编辑文件",
"Open File": "打开文件",
"Update": "更新",
"Update(Proxy)": "更新(代理)",
"Delete": "删除",
@ -41,6 +42,7 @@
"Clash core": "Clash 内核",
"Tun Mode": "Tun 模式",
"Auto Launch": "开机自启",
"Silent Start": "静默启动",
"System Proxy": "系统代理",
"Proxy Guard": "系统代理守卫",
"Proxy Bypass": "Proxy Bypass",
@ -50,5 +52,8 @@
"Language": "语言设置",
"Open App Dir": "应用目录",
"Open Logs Dir": "日志目录",
"Version": "版本"
"Version": "版本",
"Save": "保存",
"Cancel": "取消"
}

View File

@ -25,6 +25,14 @@ export async function viewProfile(index: string) {
return invoke<void>("view_profile", { index });
}
export async function readProfileFile(index: string) {
return invoke<string>("read_profile_file", { index });
}
export async function saveProfileFile(index: string, fileData: string) {
return invoke<void>("save_profile_file", { index, fileData });
}
export async function importProfile(url: string) {
return invoke<void>("import_profile", {
url,

View File

@ -1,10 +1,11 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import monaco from "vite-plugin-monaco-editor";
// https://vitejs.dev/config/
export default defineConfig({
root: "src",
plugins: [react()],
plugins: [react(), monaco()],
build: {
outDir: "../dist",
emptyOutDir: true,

View File

@ -1476,6 +1476,11 @@ minimist@^1.2.5:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
monaco-editor@^0.33.0:
version "0.33.0"
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.33.0.tgz#842e244f3750a2482f8a29c676b5684e75ff34af"
integrity sha512-VcRWPSLIUEgQJQIE0pVT8FcGBIgFoxz7jtqctE+IiCxWugD0DwgyQBcZBhdSrdMC84eumoqMZsGl2GTreOzwqw==
mri@^1.1.5:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
@ -1922,6 +1927,11 @@ universalify@^2.0.0:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
vite-plugin-monaco-editor@^1.0.10:
version "1.0.10"
resolved "https://registry.yarnpkg.com/vite-plugin-monaco-editor/-/vite-plugin-monaco-editor-1.0.10.tgz#cd370f68d4121bced6f902c6284649cc8eca4170"
integrity sha512-7yTAFIE0SefjCmfnjrvXOl53wkxeSASc/ZIcB5tZeEK3vAmHhveV8y3f90Vp8b+PYdbUipjqf91mbFbSENkpcw==
vite@^2.8.6:
version "2.8.6"
resolved "https://registry.yarnpkg.com/vite/-/vite-2.8.6.tgz#32d50e23c99ca31b26b8ccdc78b1d72d4d7323d3"