diff --git a/src/components/profile/groups-editor-viewer.tsx b/src/components/profile/groups-editor-viewer.tsx index 31a44b0..cdc7833 100644 --- a/src/components/profile/groups-editor-viewer.tsx +++ b/src/components/profile/groups-editor-viewer.tsx @@ -162,7 +162,12 @@ export const GroupsEditorViewer = (props: Props) => { useEffect(() => { if (prependSeq && appendSeq && deleteSeq) setCurrData( - yaml.dump({ prepend: prependSeq, append: appendSeq, delete: deleteSeq }) + yaml.dump( + { prepend: prependSeq, append: appendSeq, delete: deleteSeq }, + { + forceQuotes: true, + } + ) ); }, [prependSeq, appendSeq, deleteSeq]); @@ -415,7 +420,9 @@ export const GroupsEditorViewer = (props: Props) => { type="number" size="small" sx={{ minWidth: "240px" }} - {...field} + onChange={(e) => { + field.onChange(parseInt(e.target.value)); + }} /> )} @@ -431,7 +438,9 @@ export const GroupsEditorViewer = (props: Props) => { type="number" size="small" sx={{ minWidth: "240px" }} - {...field} + onChange={(e) => { + field.onChange(parseInt(e.target.value)); + }} /> )} @@ -447,7 +456,9 @@ export const GroupsEditorViewer = (props: Props) => { type="number" size="small" sx={{ minWidth: "240px" }} - {...field} + onChange={(e) => { + field.onChange(parseInt(e.target.value)); + }} /> )} @@ -478,7 +489,9 @@ export const GroupsEditorViewer = (props: Props) => { type="number" size="small" sx={{ minWidth: "240px" }} - {...field} + onChange={(e) => { + field.onChange(parseInt(e.target.value)); + }} /> )} @@ -539,7 +552,9 @@ export const GroupsEditorViewer = (props: Props) => { type="number" size="small" sx={{ minWidth: "240px" }} - {...field} + onChange={(e) => { + field.onChange(parseInt(e.target.value)); + }} /> )} diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx index 8821932..081f9d9 100644 --- a/src/components/profile/profile-item.tsx +++ b/src/components/profile/profile-item.tsx @@ -31,6 +31,7 @@ import { ProfileBox } from "./profile-box"; import parseTraffic from "@/utils/parse-traffic"; import { ConfirmViewer } from "@/components/profile/confirm-viewer"; import { open } from "@tauri-apps/api/shell"; +import { ProxiesEditorViewer } from "./proxies-editor-viewer"; const round = keyframes` from { transform: rotate(0deg); } to { transform: rotate(360deg); } @@ -492,14 +493,11 @@ export const ProfileItem = (props: Props) => { onSave={onSave} onClose={() => setRulesOpen(false)} /> - { - await saveProfileFile(option?.proxies ?? "", curr ?? ""); - onSave && onSave(prev, curr); - }} + onSave={onSave} onClose={() => setProxiesOpen(false)} /> void; + onSave?: (prev?: string, curr?: string) => void; +} + +const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"]; + +export const ProxiesEditorViewer = (props: Props) => { + const { profileUid, property, open, onClose, onSave } = props; + const { t } = useTranslation(); + const themeMode = useThemeMode(); + const [prevData, setPrevData] = useState(""); + const [currData, setCurrData] = useState(""); + const [visualization, setVisualization] = useState(true); + const [match, setMatch] = useState(() => (_: string) => true); + + const { control, watch, register, ...formIns } = useForm({ + defaultValues: { + type: "ss", + name: "", + }, + }); + + const [proxyList, setProxyList] = useState([]); + const [prependSeq, setPrependSeq] = useState([]); + const [appendSeq, setAppendSeq] = useState([]); + const [deleteSeq, setDeleteSeq] = useState([]); + + const filteredProxyList = useMemo( + () => proxyList.filter((proxy) => match(proxy.name)), + [proxyList, match] + ); + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + const reorder = ( + list: IProxyConfig[], + startIndex: number, + endIndex: number + ) => { + const result = Array.from(list); + const [removed] = result.splice(startIndex, 1); + result.splice(endIndex, 0, removed); + return result; + }; + const onPrependDragEnd = async (event: DragEndEvent) => { + const { active, over } = event; + if (over) { + if (active.id !== over.id) { + let activeIndex = 0; + let overIndex = 0; + prependSeq.forEach((item, index) => { + if (item.name === active.id) { + activeIndex = index; + } + if (item.name === over.id) { + overIndex = index; + } + }); + + setPrependSeq(reorder(prependSeq, activeIndex, overIndex)); + } + } + }; + const onAppendDragEnd = async (event: DragEndEvent) => { + const { active, over } = event; + if (over) { + if (active.id !== over.id) { + let activeIndex = 0; + let overIndex = 0; + appendSeq.forEach((item, index) => { + if (item.name === active.id) { + activeIndex = index; + } + if (item.name === over.id) { + overIndex = index; + } + }); + setAppendSeq(reorder(appendSeq, activeIndex, overIndex)); + } + } + }; + + const fetchProfile = async () => { + let data = await readProfileFile(profileUid); + + let originProxiesObj = yaml.load(data) as { + proxies: IProxyConfig[]; + } | null; + + setProxyList(originProxiesObj?.proxies || []); + }; + + const fetchContent = async () => { + let data = await readProfileFile(property); + let obj = yaml.load(data) as ISeqProfileConfig | null; + + setPrependSeq(obj?.prepend || []); + setAppendSeq(obj?.append || []); + setDeleteSeq(obj?.delete || []); + + setPrevData(data); + setCurrData(data); + }; + + useEffect(() => { + if (currData === "") return; + if (visualization !== true) return; + + let obj = yaml.load(currData) as { + prepend: []; + append: []; + delete: []; + } | null; + setPrependSeq(obj?.prepend || []); + setAppendSeq(obj?.append || []); + setDeleteSeq(obj?.delete || []); + }, [visualization]); + + useEffect(() => { + if (prependSeq && appendSeq && deleteSeq) + setCurrData( + yaml.dump( + { prepend: prependSeq, append: appendSeq, delete: deleteSeq }, + { + forceQuotes: true, + } + ) + ); + }, [prependSeq, appendSeq, deleteSeq]); + + useEffect(() => { + if (!open) return; + fetchContent(); + fetchProfile(); + }, [open]); + + const handleSave = useLockFn(async () => { + try { + await saveProfileFile(property, currData); + onSave?.(prevData, currData); + onClose(); + } catch (err: any) { + Notice.error(err.message || err.toString()); + } + }); + + return ( + + + { + + {t("Edit Proxies")} + + + + + } + + + + {visualization ? ( + <> + + + ( + + + value && field.onChange(value)} + renderInput={(params) => } + /> + + )} + /> + ( + + + + + )} + /> + ( + + + + + )} + /> + ( + + + { + field.onChange(parseInt(e.target.value)); + }} + /> + + )} + /> + + + + + + + + + + + setMatch(() => match)} + /> + 0 ? 1 : 0) + + (appendSeq.length > 0 ? 1 : 0) + } + increaseViewportBy={256} + itemContent={(index) => { + let shift = prependSeq.length > 0 ? 1 : 0; + if (prependSeq.length > 0 && index === 0) { + return ( + + { + return x.name; + })} + > + {prependSeq.map((item, index) => { + return ( + { + setPrependSeq( + prependSeq.filter( + (v) => v.name !== item.name + ) + ); + }} + /> + ); + })} + + + ); + } else if (index < filteredProxyList.length + shift) { + let newIndex = index - shift; + return ( + { + if ( + deleteSeq.includes(filteredProxyList[newIndex].name) + ) { + setDeleteSeq( + deleteSeq.filter( + (v) => v !== filteredProxyList[newIndex].name + ) + ); + } else { + setDeleteSeq((prev) => [ + ...prev, + filteredProxyList[newIndex].name, + ]); + } + }} + /> + ); + } else { + return ( + + { + return x.name; + })} + > + {appendSeq.map((item, index) => { + return ( + { + setAppendSeq( + appendSeq.filter( + (v) => v.name !== item.name + ) + ); + }} + /> + ); + })} + + + ); + } + }} + /> + + + ) : ( + = 1500, // 超过一定宽度显示minimap滚动条 + }, + mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例 + 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, // 平滑滚动 + }} + onChange={(value) => setCurrData(value)} + /> + )} + + + + + + + + + ); +}; + +const Item = styled(ListItem)(() => ({ + padding: "5px 2px", +})); diff --git a/src/components/profile/proxy-item.tsx b/src/components/profile/proxy-item.tsx new file mode 100644 index 0000000..5de1028 --- /dev/null +++ b/src/components/profile/proxy-item.tsx @@ -0,0 +1,116 @@ +import { + Box, + IconButton, + ListItem, + ListItemText, + alpha, + styled, +} from "@mui/material"; +import { DeleteForeverRounded, UndoRounded } from "@mui/icons-material"; +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; + +interface Props { + type: "prepend" | "original" | "delete" | "append"; + proxy: IProxyConfig; + onDelete: () => void; +} + +export const ProxyItem = (props: Props) => { + let { type, proxy, onDelete } = props; + const sortable = type === "prepend" || type === "append"; + + const { attributes, listeners, setNodeRef, transform, transition } = sortable + ? useSortable({ id: proxy.name }) + : { + attributes: {}, + listeners: {}, + setNodeRef: null, + transform: null, + transition: null, + }; + + return ( + ({ + background: + type === "original" + ? palette.mode === "dark" + ? alpha(palette.background.paper, 0.3) + : alpha(palette.grey[400], 0.3) + : type === "delete" + ? alpha(palette.error.main, 0.3) + : alpha(palette.success.main, 0.3), + height: "100%", + margin: "8px 0", + borderRadius: "8px", + transform: CSS.Transform.toString(transform), + transition, + })} + > + + {proxy.name} + + } + secondary={ + + + {proxy.type} + + + } + secondaryTypographyProps={{ + sx: { + display: "flex", + alignItems: "center", + color: "#ccc", + }, + }} + /> + + {type === "delete" ? : } + + + ); +}; + +const StyledPrimary = styled("span")` + font-size: 15px; + font-weight: 700; + line-height: 1.5; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const ListItemTextChild = styled("span")` + display: block; +`; + +const StyledTypeBox = styled(ListItemTextChild)(({ theme }) => ({ + display: "inline-block", + border: "1px solid #ccc", + borderColor: alpha(theme.palette.primary.main, 0.5), + color: alpha(theme.palette.primary.main, 0.8), + borderRadius: 4, + fontSize: 10, + padding: "0 4px", + lineHeight: 1.5, + marginRight: "8px", +})); diff --git a/src/components/profile/rules-editor-viewer.tsx b/src/components/profile/rules-editor-viewer.tsx index 5213934..99e20db 100644 --- a/src/components/profile/rules-editor-viewer.tsx +++ b/src/components/profile/rules-editor-viewer.tsx @@ -316,7 +316,12 @@ export const RulesEditorViewer = (props: Props) => { useEffect(() => { if (prependSeq && appendSeq && deleteSeq) setCurrData( - yaml.dump({ prepend: prependSeq, append: appendSeq, delete: deleteSeq }) + yaml.dump( + { prepend: prependSeq, append: appendSeq, delete: deleteSeq }, + { + forceQuotes: true, + } + ) ); }, [prependSeq, appendSeq, deleteSeq]); diff --git a/src/services/types.d.ts b/src/services/types.d.ts index 777a63b..b2b3815 100644 --- a/src/services/types.d.ts +++ b/src/services/types.d.ts @@ -228,6 +228,35 @@ interface IProxyGroupConfig { icon?: string; } +interface IProxyConfig { + name: string; + type: + | "ss" + | "ssr" + | "direct" + | "dns" + | "snell" + | "http" + | "trojan" + | "hysteria" + | "hysteria2" + | "tuic" + | "wireguard" + | "ssh" + | "socks5" + | "vmess" + | "vless"; + server: string; + port: number; + "ip-version"?: string; + udp?: boolean; + "interface-name"?: string; + "routing-mark"?: number; + tfo?: boolean; + mptcp?: boolean; + "dialer-proxy"?: string; +} + interface IVergeConfig { app_log_level?: "trace" | "debug" | "info" | "warn" | "error" | string; language?: string;