Browse Source

feat: display network interface

MystiPanda 11 months ago
parent
commit
48f7c15035

+ 14 - 0
src-tauri/Cargo.lock

@@ -803,6 +803,7 @@ dependencies = [
  "log 0.4.22",
  "log4rs",
  "nanoid",
+ "network-interface",
  "once_cell",
  "open 5.2.0",
  "parking_lot",
@@ -3243,6 +3244,19 @@ dependencies = [
  "jni-sys",
 ]
 
+[[package]]
+name = "network-interface"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "433419f898328beca4f2c6c73a1b52540658d92b0a99f0269330457e0fd998d5"
+dependencies = [
+ "cc",
+ "libc",
+ "serde",
+ "thiserror",
+ "winapi",
+]
+
 [[package]]
 name = "new_debug_unreachable"
 version = "1.0.6"

+ 1 - 0
src-tauri/Cargo.toml

@@ -38,6 +38,7 @@ serde = { version = "1.0", features = ["derive"] }
 reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
 sysproxy = { git="https://github.com/zzzgydi/sysproxy-rs", branch = "main" }
 tauri = { version="1", features = [ "fs-read-file", "fs-exists", "path-all", "protocol-asset", "dialog-open", "notification-all", "icon-png", "icon-ico", "clipboard-all", "global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all", "devtools"] }
+network-interface = { version = "2.0.0", features = ["serde"] }
 [target.'cfg(windows)'.dependencies]
 runas = "=1.2.0"
 deelevate = "0.2.0"

+ 20 - 0
src-tauri/src/cmds.rs

@@ -6,6 +6,7 @@ use crate::{
 };
 use crate::{ret_err, wrap_err};
 use anyhow::{Context, Result};
+use network_interface::NetworkInterface;
 use serde_yaml::Mapping;
 use std::collections::{HashMap, VecDeque};
 use sysproxy::{Autoproxy, Sysproxy};
@@ -339,6 +340,25 @@ pub fn get_network_interfaces() -> Vec<String> {
     return result;
 }
 
+#[tauri::command]
+pub fn get_network_interfaces_info() -> CmdResult<Vec<NetworkInterface>> {
+    use network_interface::NetworkInterface;
+    use network_interface::NetworkInterfaceConfig;
+
+    let names = get_network_interfaces();
+    let interfaces = wrap_err!(NetworkInterface::show())?;
+
+    let mut result = Vec::new();
+
+    for interface in interfaces {
+        if names.contains(&interface.name) {
+            result.push(interface);
+        }
+    }
+
+    Ok(result)
+}
+
 #[tauri::command]
 pub fn open_devtools(app_handle: tauri::AppHandle) {
     if let Some(window) = app_handle.get_window("main") {

+ 1 - 0
src-tauri/src/main.rs

@@ -74,6 +74,7 @@ fn main() -> std::io::Result<()> {
             cmds::download_icon_cache,
             cmds::open_devtools,
             cmds::exit_app,
+            cmds::get_network_interfaces_info,
             // cmds::update_hotkeys,
             // profile
             cmds::get_profiles,

+ 135 - 0
src/components/setting/mods/network-interface-viewer.tsx

@@ -0,0 +1,135 @@
+import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { BaseDialog, DialogRef, Notice } from "@/components/base";
+import { getNetworkInterfacesInfo } from "@/services/cmds";
+import { alpha, Box, Button, Chip, IconButton } from "@mui/material";
+import { ContentCopyRounded } from "@mui/icons-material";
+import { writeText } from "@tauri-apps/api/clipboard";
+
+export const NetworkInterfaceViewer = forwardRef<DialogRef>((props, ref) => {
+  const { t } = useTranslation();
+  const [open, setOpen] = useState(false);
+  const [networkInterfaces, setNetworkInterfaces] = useState<
+    INetworkInterface[]
+  >([]);
+  const [isV4, setIsV4] = useState(true);
+
+  useImperativeHandle(ref, () => ({
+    open: () => {
+      setOpen(true);
+    },
+    close: () => setOpen(false),
+  }));
+
+  useEffect(() => {
+    if (!open) return;
+    getNetworkInterfacesInfo().then((res) => {
+      console.log(res);
+      setNetworkInterfaces(res);
+    });
+  }, [open]);
+
+  return (
+    <BaseDialog
+      open={open}
+      title={
+        <Box display="flex" justifyContent="space-between">
+          {t("Network Interface")}
+          <Box>
+            <Button
+              variant="contained"
+              size="small"
+              onClick={() => {
+                setIsV4((prev) => !prev);
+              }}
+            >
+              {isV4 ? t("Ipv6") : t("Ipv4")}
+            </Button>
+          </Box>
+        </Box>
+      }
+      contentSx={{ width: 450, maxHeight: 330 }}
+      okBtn={t("Save")}
+      cancelBtn={t("Cancel")}
+      onClose={() => setOpen(false)}
+      onCancel={() => setOpen(false)}
+    >
+      {networkInterfaces.map((item) => (
+        <Box key={item.name}>
+          <h4>{item.name}</h4>
+          <Box>
+            {isV4 && (
+              <>
+                {item.addr.map(
+                  (address) =>
+                    address.V4 && (
+                      <AddressDisplay
+                        key={address.V4.ip}
+                        label="Address"
+                        content={address.V4.ip}
+                      />
+                    )
+                )}
+                <AddressDisplay label="Mac" content={item.mac_addr ?? ""} />
+              </>
+            )}
+            {!isV4 && (
+              <>
+                {item.addr.map(
+                  (address) =>
+                    address.V6 && (
+                      <AddressDisplay
+                        key={address.V6.ip}
+                        label="Address"
+                        content={address.V6.ip}
+                      />
+                    )
+                )}
+                <AddressDisplay label="Mac" content={item.mac_addr ?? ""} />
+              </>
+            )}
+          </Box>
+        </Box>
+      ))}
+    </BaseDialog>
+  );
+});
+
+const AddressDisplay = (props: { label: string; content: string }) => {
+  const { t } = useTranslation();
+
+  return (
+    <Box
+      sx={{
+        display: "flex",
+        justifyContent: "space-between",
+        margin: "8px 0",
+      }}
+    >
+      <Box>{props.label}</Box>
+      <Box
+        sx={({ palette }) => ({
+          borderRadius: "8px",
+          padding: "2px",
+          background:
+            palette.mode === "dark"
+              ? alpha(palette.background.paper, 0.3)
+              : alpha(palette.grey[400], 0.3),
+        })}
+      >
+        <Box sx={{ display: "inline", userSelect: "text" }}>
+          {props.content}
+        </Box>
+        <IconButton
+          size="small"
+          onClick={async () => {
+            await writeText(props.content);
+            Notice.success(t("Copy Success"));
+          }}
+        >
+          <ContentCopyRounded sx={{ fontSize: "18px" }} />
+        </IconButton>
+      </Box>
+    </Box>
+  );
+};

+ 23 - 4
src/components/setting/setting-clash.tsx

@@ -2,7 +2,11 @@ import { useRef } from "react";
 import { useTranslation } from "react-i18next";
 import { TextField, Select, MenuItem, Typography } from "@mui/material";
 
-import { Settings, Shuffle } from "@mui/icons-material";
+import {
+  SettingsRounded,
+  ShuffleRounded,
+  LanRounded,
+} from "@mui/icons-material";
 import { DialogRef, Notice, Switch } from "@/components/base";
 import { useClash } from "@/hooks/use-clash";
 import { GuardState } from "./mods/guard-state";
@@ -16,6 +20,7 @@ import getSystem from "@/utils/get-system";
 import { useVerge } from "@/hooks/use-verge";
 import { updateGeoData } from "@/services/api";
 import { TooltipIcon } from "@/components/base/base-tooltip-icon";
+import { NetworkInterfaceViewer } from "./mods/network-interface-viewer";
 
 const isWIN = getSystem() === "windows";
 
@@ -37,6 +42,7 @@ const SettingClash = ({ onError }: Props) => {
   const portRef = useRef<DialogRef>(null);
   const ctrlRef = useRef<DialogRef>(null);
   const coreRef = useRef<DialogRef>(null);
+  const networkRef = useRef<DialogRef>(null);
 
   const onSwitchFormat = (_e: any, value: boolean) => value;
   const onChangeData = (patch: Partial<IConfigData>) => {
@@ -60,8 +66,21 @@ const SettingClash = ({ onError }: Props) => {
       <ClashPortViewer ref={portRef} />
       <ControllerViewer ref={ctrlRef} />
       <ClashCoreViewer ref={coreRef} />
+      <NetworkInterfaceViewer ref={networkRef} />
 
-      <SettingItem label={t("Allow Lan")}>
+      <SettingItem
+        label={t("Allow Lan")}
+        extra={
+          <TooltipIcon
+            title={t("Network Interface")}
+            color={"inherit"}
+            icon={LanRounded}
+            onClick={() => {
+              networkRef.current?.open();
+            }}
+          />
+        }
+      >
         <GuardState
           value={allowLan ?? false}
           valueProps="checked"
@@ -112,7 +131,7 @@ const SettingClash = ({ onError }: Props) => {
           <TooltipIcon
             title={t("Random Port")}
             color={enable_random_port ? "primary" : "inherit"}
-            icon={Shuffle}
+            icon={ShuffleRounded}
             onClick={() => {
               Notice.success(
                 t("Restart Application to Apply Modifications"),
@@ -148,7 +167,7 @@ const SettingClash = ({ onError }: Props) => {
         label={t("Clash Core")}
         extra={
           <TooltipIcon
-            icon={Settings}
+            icon={SettingsRounded}
             onClick={() => coreRef.current?.open()}
           />
         }

+ 4 - 0
src/services/cmds.ts

@@ -241,3 +241,7 @@ export async function downloadIconCache(url: string, name: string) {
 export async function getNetworkInterfaces() {
   return invoke<string[]>("get_network_interfaces");
 }
+
+export async function getNetworkInterfacesInfo() {
+  return invoke<INetworkInterface[]>("get_network_interfaces_info");
+}

+ 18 - 0
src/services/types.d.ts

@@ -197,6 +197,24 @@ interface IVergeTestItem {
   icon?: string;
   url: string;
 }
+interface IAddress {
+  V4?: {
+    ip: string;
+    broadcast?: string;
+    netmask?: string;
+  };
+  V6?: {
+    ip: string;
+    broadcast?: string;
+    netmask?: string;
+  };
+}
+interface INetworkInterface {
+  name: string;
+  addr: IAddress[];
+  mac_addr?: string;
+  index: number;
+}
 
 interface ISeqProfileConfig {
   prepend: [];