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

View File

@ -212,6 +212,35 @@ pub fn view_profile(index: String, profiles_state: State<'_, ProfilesState>) ->
wrap_err!(open::that(path)) 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 /// restart the sidecar
#[tauri::command] #[tauri::command]
pub fn restart_sidecar( pub fn restart_sidecar(

View File

@ -279,6 +279,28 @@ impl PrfItem {
file_data: Some(tmpl::ITEM_SCRIPT.into()), 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::get_profiles,
cmds::sync_profiles, cmds::sync_profiles,
cmds::enhance_profiles, cmds::enhance_profiles,
cmds::change_profile_chain cmds::change_profile_chain,
cmds::read_profile_file,
cmds::save_profile_file
]); ]);
#[cfg(target_os = "macos")] #[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 { updateProfile, deleteProfile, viewProfile } from "../../services/cmds";
import parseTraffic from "../../utils/parse-traffic"; import parseTraffic from "../../utils/parse-traffic";
import ProfileEdit from "./profile-edit"; import ProfileEdit from "./profile-edit";
import FileEditor from "./file-editor";
import Notice from "../base/base-notice"; import Notice from "../base/base-notice";
const Wrapper = styled(Box)(({ theme }) => ({ const Wrapper = styled(Box)(({ theme }) => ({
@ -54,7 +55,7 @@ const ProfileItem = (props: Props) => {
const [position, setPosition] = useState({ left: 0, top: 0 }); const [position, setPosition] = useState({ left: 0, top: 0 });
const [loadingCache, setLoadingCache] = useRecoilState(atomLoadingCache); 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 { upload = 0, download = 0, total = 0 } = extra ?? {};
const from = parseUrl(itemData.url); const from = parseUrl(itemData.url);
const expire = parseExpire(extra?.expire); const expire = parseExpire(extra?.expire);
@ -70,18 +71,16 @@ const ProfileItem = (props: Props) => {
const loading = loadingCache[itemData.uid] ?? false; const loading = loadingCache[itemData.uid] ?? false;
const [editOpen, setEditOpen] = useState(false); const [editOpen, setEditOpen] = useState(false);
const onEdit = () => { const [fileOpen, setFileOpen] = useState(false);
const onEditInfo = () => {
setAnchorEl(null); setAnchorEl(null);
setEditOpen(true); setEditOpen(true);
}; };
const onView = async () => { const onEditFile = () => {
setAnchorEl(null); setAnchorEl(null);
try { setFileOpen(true);
await viewProfile(itemData.uid);
} catch (err: any) {
Notice.error(err?.message || err.toString());
}
}; };
const onForceSelect = () => { const onForceSelect = () => {
@ -89,6 +88,15 @@ const ProfileItem = (props: Props) => {
onSelect(true); 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) => { const onUpdate = useLockFn(async (withProxy: boolean) => {
setAnchorEl(null); setAnchorEl(null);
setLoadingCache((cache) => ({ ...cache, [itemData.uid]: true })); setLoadingCache((cache) => ({ ...cache, [itemData.uid]: true }));
@ -122,16 +130,18 @@ const ProfileItem = (props: Props) => {
const urlModeMenu = [ const urlModeMenu = [
{ label: "Select", handler: onForceSelect }, { label: "Select", handler: onForceSelect },
{ label: "Edit", handler: onEdit }, { label: "Edit Info", handler: onEditInfo },
{ label: "File", handler: onView }, { label: "Edit File", handler: onEditFile },
{ label: "Open File", handler: onOpenFile },
{ label: "Update", handler: () => onUpdate(false) }, { label: "Update", handler: () => onUpdate(false) },
{ label: "Update(Proxy)", handler: () => onUpdate(true) }, { label: "Update(Proxy)", handler: () => onUpdate(true) },
{ label: "Delete", handler: onDelete }, { label: "Delete", handler: onDelete },
]; ];
const fileModeMenu = [ const fileModeMenu = [
{ label: "Select", handler: onForceSelect }, { label: "Select", handler: onForceSelect },
{ label: "Edit", handler: onEdit }, { label: "Edit Info", handler: onEditInfo },
{ label: "File", handler: onView }, { label: "Edit File", handler: onEditFile },
{ label: "Open File", handler: onOpenFile },
{ label: "Delete", handler: onDelete }, { label: "Delete", handler: onDelete },
]; ];
@ -256,6 +266,7 @@ const ProfileItem = (props: Props) => {
onClose={() => setAnchorEl(null)} onClose={() => setAnchorEl(null)}
anchorPosition={position} anchorPosition={position}
anchorReference="anchorPosition" anchorReference="anchorPosition"
transitionDuration={225}
onContextMenu={(e) => { onContextMenu={(e) => {
setAnchorEl(null); setAnchorEl(null);
e.preventDefault(); e.preventDefault();
@ -279,6 +290,15 @@ const ProfileItem = (props: Props) => {
onClose={() => setEditOpen(false)} 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 dayjs from "dayjs";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
import { import {
alpha, alpha,
Box, Box,
@ -12,9 +13,10 @@ import {
} from "@mui/material"; } from "@mui/material";
import { CmdType } from "../../services/types"; import { CmdType } from "../../services/types";
import { viewProfile } from "../../services/cmds"; import { viewProfile } from "../../services/cmds";
import ProfileEdit from "./profile-edit";
import Notice from "../base/base-notice";
import enhance from "../../services/enhance"; 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 }) => ({ const Wrapper = styled(Box)(({ theme }) => ({
width: "100%", width: "100%",
@ -57,6 +59,7 @@ const ProfileMore = (props: Props) => {
const [anchorEl, setAnchorEl] = useState<any>(null); const [anchorEl, setAnchorEl] = useState<any>(null);
const [position, setPosition] = useState({ left: 0, top: 0 }); const [position, setPosition] = useState({ left: 0, top: 0 });
const [editOpen, setEditOpen] = useState(false); const [editOpen, setEditOpen] = useState(false);
const [fileOpen, setFileOpen] = useState(false);
const [status, setStatus] = useState(enhance.status(uid)); const [status, setStatus] = useState(enhance.status(uid));
// unlisten when unmount // unlisten when unmount
@ -65,40 +68,47 @@ const ProfileMore = (props: Props) => {
// error during enhanced mode // error during enhanced mode
const hasError = selected && status?.status === "error"; const hasError = selected && status?.status === "error";
const onEdit = () => { const onEditInfo = () => {
setAnchorEl(null); setAnchorEl(null);
setEditOpen(true); setEditOpen(true);
}; };
const onView = async () => { const onEditFile = () => {
setAnchorEl(null);
setFileOpen(true);
};
const onOpenFile = useLockFn(async () => {
setAnchorEl(null); setAnchorEl(null);
try { try {
await viewProfile(itemData.uid); await viewProfile(itemData.uid);
} catch (err: any) { } catch (err: any) {
Notice.error(err?.message || err.toString()); Notice.error(err?.message || err.toString());
} }
}; });
const closeWrapper = (fn: () => void) => () => { const fnWrapper = (fn: () => void) => () => {
setAnchorEl(null); setAnchorEl(null);
return fn(); return fn();
}; };
const enableMenu = [ const enableMenu = [
{ label: "Disable", handler: closeWrapper(onDisable) }, { label: "Disable", handler: fnWrapper(onDisable) },
{ label: "Refresh", handler: closeWrapper(onEnhance) }, { label: "Refresh", handler: fnWrapper(onEnhance) },
{ label: "Edit", handler: onEdit }, { label: "Edit Info", handler: onEditInfo },
{ label: "File", handler: onView }, { label: "Edit File", handler: onEditFile },
{ label: "To Top", show: !hasError, handler: closeWrapper(onMoveTop) }, { label: "Open File", handler: onOpenFile },
{ label: "To End", show: !hasError, handler: closeWrapper(onMoveEnd) }, { label: "To Top", show: !hasError, handler: fnWrapper(onMoveTop) },
{ label: "Delete", handler: closeWrapper(onDelete) }, { label: "To End", show: !hasError, handler: fnWrapper(onMoveEnd) },
{ label: "Delete", handler: fnWrapper(onDelete) },
]; ];
const disableMenu = [ const disableMenu = [
{ label: "Enable", handler: closeWrapper(onEnable) }, { label: "Enable", handler: fnWrapper(onEnable) },
{ label: "Edit", handler: onEdit }, { label: "Edit Info", handler: onEditInfo },
{ label: "File", handler: onView }, { label: "Edit File", handler: onEditFile },
{ label: "Delete", handler: closeWrapper(onDelete) }, { label: "Open File", handler: onOpenFile },
{ label: "Delete", handler: fnWrapper(onDelete) },
]; ];
const boxStyle = { const boxStyle = {
@ -208,6 +218,7 @@ const ProfileMore = (props: Props) => {
onClose={() => setAnchorEl(null)} onClose={() => setAnchorEl(null)}
anchorPosition={position} anchorPosition={position}
anchorReference="anchorPosition" anchorReference="anchorPosition"
transitionDuration={225}
onContextMenu={(e) => { onContextMenu={(e) => {
setAnchorEl(null); setAnchorEl(null);
e.preventDefault(); e.preventDefault();
@ -233,6 +244,15 @@ const ProfileMore = (props: Props) => {
onClose={() => setEditOpen(false)} 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", "New": "New",
"Close All": "Close All", "Close All": "Close All",
"Select": "Select", "Select": "Select",
"Edit": "Edit", "Edit Info": "Edit Info",
"File": "File", "Edit File": "Edit File",
"Open File": "Open File",
"Update": "Update", "Update": "Update",
"Update(Proxy)": "Update(Proxy)", "Update(Proxy)": "Update(Proxy)",
"Delete": "Delete", "Delete": "Delete",
@ -41,6 +42,7 @@
"Clash core": "Clash core", "Clash core": "Clash core",
"Tun Mode": "Tun Mode", "Tun Mode": "Tun Mode",
"Auto Launch": "Auto Launch", "Auto Launch": "Auto Launch",
"Silent Start": "Silent Start",
"System Proxy": "System Proxy", "System Proxy": "System Proxy",
"Proxy Guard": "Proxy Guard", "Proxy Guard": "Proxy Guard",
"Proxy Bypass": "Proxy Bypass", "Proxy Bypass": "Proxy Bypass",
@ -50,5 +52,8 @@
"Language": "Language", "Language": "Language",
"Open App Dir": "Open App Dir", "Open App Dir": "Open App Dir",
"Open Logs Dir": "Open Logs Dir", "Open Logs Dir": "Open Logs Dir",
"Version": "Version" "Version": "Version",
"Save": "Save",
"Cancel": "Cancel"
} }

View File

@ -19,8 +19,9 @@
"New": "新建", "New": "新建",
"Close All": "关闭全部", "Close All": "关闭全部",
"Select": "使用", "Select": "使用",
"Edit": "编辑信息", "Edit Info": "编辑信息",
"File": "打开文件", "Edit File": "编辑文件",
"Open File": "打开文件",
"Update": "更新", "Update": "更新",
"Update(Proxy)": "更新(代理)", "Update(Proxy)": "更新(代理)",
"Delete": "删除", "Delete": "删除",
@ -41,6 +42,7 @@
"Clash core": "Clash 内核", "Clash core": "Clash 内核",
"Tun Mode": "Tun 模式", "Tun Mode": "Tun 模式",
"Auto Launch": "开机自启", "Auto Launch": "开机自启",
"Silent Start": "静默启动",
"System Proxy": "系统代理", "System Proxy": "系统代理",
"Proxy Guard": "系统代理守卫", "Proxy Guard": "系统代理守卫",
"Proxy Bypass": "Proxy Bypass", "Proxy Bypass": "Proxy Bypass",
@ -50,5 +52,8 @@
"Language": "语言设置", "Language": "语言设置",
"Open App Dir": "应用目录", "Open App Dir": "应用目录",
"Open Logs 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 }); 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) { export async function importProfile(url: string) {
return invoke<void>("import_profile", { return invoke<void>("import_profile", {
url, url,

View File

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

View File

@ -1476,6 +1476,11 @@ minimist@^1.2.5:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== 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: mri@^1.1.5:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" 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" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== 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: vite@^2.8.6:
version "2.8.6" version "2.8.6"
resolved "https://registry.yarnpkg.com/vite/-/vite-2.8.6.tgz#32d50e23c99ca31b26b8ccdc78b1d72d4d7323d3" resolved "https://registry.yarnpkg.com/vite/-/vite-2.8.6.tgz#32d50e23c99ca31b26b8ccdc78b1d72d4d7323d3"