Browse Source

feat: global merge and script

MystiPanda 11 months ago
parent
commit
b1444b8635

+ 17 - 0
src-tauri/src/config/config.rs

@@ -1,5 +1,6 @@
 use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
 use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
 use crate::{
 use crate::{
+    config::PrfItem,
     enhance,
     enhance,
     utils::{dirs, help},
     utils::{dirs, help},
 };
 };
@@ -47,6 +48,22 @@ impl Config {
 
 
     /// 初始化订阅
     /// 初始化订阅
     pub async fn init_config() -> Result<()> {
     pub async fn init_config() -> Result<()> {
+        if Self::profiles()
+            .data()
+            .get_item(&"Merge".to_string())
+            .is_err()
+        {
+            let merge_item = PrfItem::from_merge(Some("Merge".to_string()))?;
+            Self::profiles().data().append_item(merge_item.clone())?;
+        }
+        if Self::profiles()
+            .data()
+            .get_item(&"Script".to_string())
+            .is_err()
+        {
+            let script_item = PrfItem::from_script(Some("Script".to_string()))?;
+            Self::profiles().data().append_item(script_item.clone())?;
+        }
         crate::log_err!(Self::generate().await);
         crate::log_err!(Self::generate().await);
         if let Err(err) = Self::generate_file(ConfigType::Run) {
         if let Err(err) = Self::generate_file(ConfigType::Run) {
             log::error!(target: "app", "{err}");
             log::error!(target: "app", "{err}");

+ 18 - 12
src-tauri/src/config/prfitem.rs

@@ -175,12 +175,12 @@ impl PrfItem {
         let mut groups = opt_ref.and_then(|o| o.groups.clone());
         let mut groups = opt_ref.and_then(|o| o.groups.clone());
 
 
         if merge.is_none() {
         if merge.is_none() {
-            let merge_item = PrfItem::from_merge()?;
+            let merge_item = PrfItem::from_merge(None)?;
             Config::profiles().data().append_item(merge_item.clone())?;
             Config::profiles().data().append_item(merge_item.clone())?;
             merge = merge_item.uid;
             merge = merge_item.uid;
         }
         }
         if script.is_none() {
         if script.is_none() {
-            let script_item = PrfItem::from_script()?;
+            let script_item = PrfItem::from_script(None)?;
             Config::profiles().data().append_item(script_item.clone())?;
             Config::profiles().data().append_item(script_item.clone())?;
             script = script_item.uid;
             script = script_item.uid;
         }
         }
@@ -248,12 +248,12 @@ impl PrfItem {
         let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
         let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
 
 
         if merge.is_none() {
         if merge.is_none() {
-            let merge_item = PrfItem::from_merge()?;
+            let merge_item = PrfItem::from_merge(None)?;
             Config::profiles().data().append_item(merge_item.clone())?;
             Config::profiles().data().append_item(merge_item.clone())?;
             merge = merge_item.uid;
             merge = merge_item.uid;
         }
         }
         if script.is_none() {
         if script.is_none() {
-            let script_item = PrfItem::from_script()?;
+            let script_item = PrfItem::from_script(None)?;
             Config::profiles().data().append_item(script_item.clone())?;
             Config::profiles().data().append_item(script_item.clone())?;
             script = script_item.uid;
             script = script_item.uid;
         }
         }
@@ -426,12 +426,15 @@ impl PrfItem {
 
 
     /// ## Merge type (enhance)
     /// ## Merge type (enhance)
     /// create the enhanced item by using `merge` rule
     /// create the enhanced item by using `merge` rule
-    pub fn from_merge() -> Result<PrfItem> {
-        let uid = help::get_uid("m");
-        let file = format!("{uid}.yaml");
+    pub fn from_merge(uid: Option<String>) -> Result<PrfItem> {
+        let mut id = help::get_uid("m");
+        if let Some(uid) = uid {
+            id = uid;
+        }
+        let file = format!("{id}.yaml");
 
 
         Ok(PrfItem {
         Ok(PrfItem {
-            uid: Some(uid),
+            uid: Some(id),
             itype: Some("merge".into()),
             itype: Some("merge".into()),
             name: None,
             name: None,
             desc: None,
             desc: None,
@@ -448,12 +451,15 @@ impl PrfItem {
 
 
     /// ## Script type (enhance)
     /// ## Script type (enhance)
     /// create the enhanced item by using javascript quick.js
     /// create the enhanced item by using javascript quick.js
-    pub fn from_script() -> Result<PrfItem> {
-        let uid = help::get_uid("s");
-        let file = format!("{uid}.js"); // js ext
+    pub fn from_script(uid: Option<String>) -> Result<PrfItem> {
+        let mut id = help::get_uid("s");
+        if let Some(uid) = uid {
+            id = uid;
+        }
+        let file = format!("{id}.js"); // js ext
 
 
         Ok(PrfItem {
         Ok(PrfItem {
-            uid: Some(uid),
+            uid: Some(id),
             itype: Some("script".into()),
             itype: Some("script".into()),
             name: None,
             name: None,
             desc: None,
             desc: None,

+ 59 - 2
src-tauri/src/enhance/mod.rs

@@ -50,7 +50,16 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
     };
     };
 
 
     // 从profiles里拿东西
     // 从profiles里拿东西
-    let (mut config, merge_item, script_item, rules_item, proxies_item, groups_item) = {
+    let (
+        mut config,
+        merge_item,
+        script_item,
+        rules_item,
+        proxies_item,
+        groups_item,
+        global_merge,
+        global_script,
+    ) = {
         let profiles = Config::profiles();
         let profiles = Config::profiles();
         let profiles = profiles.latest();
         let profiles = profiles.latest();
 
 
@@ -96,7 +105,34 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
                 data: ChainType::Groups(SeqMap::default()),
                 data: ChainType::Groups(SeqMap::default()),
             });
             });
 
 
-        (current, merge, script, rules, proxies, groups)
+        let global_merge = profiles
+            .get_item(&"Merge".to_string())
+            .ok()
+            .and_then(<Option<ChainItem>>::from)
+            .unwrap_or_else(|| ChainItem {
+                uid: "Merge".into(),
+                data: ChainType::Merge(Mapping::new()),
+            });
+
+        let global_script = profiles
+            .get_item(&"Script".to_string())
+            .ok()
+            .and_then(<Option<ChainItem>>::from)
+            .unwrap_or_else(|| ChainItem {
+                uid: "Script".into(),
+                data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
+            });
+
+        (
+            current,
+            merge,
+            script,
+            rules,
+            proxies,
+            groups,
+            global_merge,
+            global_script,
+        )
     };
     };
 
 
     let mut result_map = HashMap::new(); // 保存脚本日志
     let mut result_map = HashMap::new(); // 保存脚本日志
@@ -136,6 +172,27 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
         result_map.insert(script_item.uid, logs);
         result_map.insert(script_item.uid, logs);
     }
     }
 
 
+    // 全局Merge和Script
+    if let ChainType::Merge(merge) = global_merge.data {
+        exists_keys.extend(use_keys(&merge));
+        config = use_merge(merge, config.to_owned());
+    }
+
+    if let ChainType::Script(script) = global_script.data {
+        let mut logs = vec![];
+
+        match use_script(script, config.to_owned()) {
+            Ok((res_config, res_logs)) => {
+                exists_keys.extend(use_keys(&res_config));
+                config = res_config;
+                logs.extend(res_logs);
+            }
+            Err(err) => logs.push(("exception".into(), err.to_string())),
+        }
+
+        result_map.insert(global_script.uid, logs);
+    }
+
     // 合并默认的config
     // 合并默认的config
     for (key, value) in clash_config.into_iter() {
     for (key, value) in clash_config.into_iter() {
         if key.as_str() == Some("tun") {
         if key.as_str() == Some("tun") {

+ 197 - 0
src/components/profile/profile-more.tsx

@@ -0,0 +1,197 @@
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { useLockFn } from "ahooks";
+import {
+  Box,
+  Badge,
+  Chip,
+  Typography,
+  MenuItem,
+  Menu,
+  IconButton,
+} from "@mui/material";
+import { FeaturedPlayListRounded } from "@mui/icons-material";
+import { viewProfile } from "@/services/cmds";
+import { Notice } from "@/components/base";
+import { EditorViewer } from "@/components/profile/editor-viewer";
+import { ProfileBox } from "./profile-box";
+import { LogViewer } from "./log-viewer";
+
+interface Props {
+  logInfo?: [string, string][];
+  id: "Merge" | "Script";
+  onChange?: (prev?: string, curr?: string) => void;
+}
+
+// profile enhanced item
+export const ProfileMore = (props: Props) => {
+  const { id, logInfo = [], onChange } = props;
+
+  const { t, i18n } = useTranslation();
+  const [anchorEl, setAnchorEl] = useState<any>(null);
+  const [position, setPosition] = useState({ left: 0, top: 0 });
+  const [fileOpen, setFileOpen] = useState(false);
+  const [logOpen, setLogOpen] = useState(false);
+
+  const onEditFile = () => {
+    setAnchorEl(null);
+    setFileOpen(true);
+  };
+
+  const onOpenFile = useLockFn(async () => {
+    setAnchorEl(null);
+    try {
+      await viewProfile(id);
+    } catch (err: any) {
+      Notice.error(err?.message || err.toString());
+    }
+  });
+
+  const fnWrapper = (fn: () => void) => () => {
+    setAnchorEl(null);
+    return fn();
+  };
+
+  const hasError = !!logInfo.find((e) => e[0] === "exception");
+
+  const itemMenu = [
+    { label: "Edit File", handler: onEditFile },
+    { label: "Open File", handler: onOpenFile },
+  ];
+
+  const boxStyle = {
+    height: 26,
+    display: "flex",
+    alignItems: "center",
+    justifyContent: "space-between",
+    lineHeight: 1,
+  };
+
+  return (
+    <>
+      <ProfileBox
+        onDoubleClick={onEditFile}
+        onContextMenu={(event) => {
+          const { clientX, clientY } = event;
+          setPosition({ top: clientY, left: clientX });
+          setAnchorEl(event.currentTarget);
+          event.preventDefault();
+        }}
+      >
+        <Box
+          display="flex"
+          justifyContent="space-between"
+          alignItems="center"
+          mb={0.5}
+        >
+          <Typography
+            width="calc(100% - 52px)"
+            variant="h6"
+            component="h2"
+            noWrap
+            title={t(`Global ${id}`)}
+          >
+            {t(`Global ${id}`)}
+          </Typography>
+
+          <Chip
+            label={id}
+            color="primary"
+            size="small"
+            variant="outlined"
+            sx={{ height: 20, textTransform: "capitalize" }}
+          />
+        </Box>
+
+        <Box sx={boxStyle}>
+          {id === "Script" ? (
+            hasError ? (
+              <Badge color="error" variant="dot" overlap="circular">
+                <IconButton
+                  size="small"
+                  edge="start"
+                  color="error"
+                  title={t("Script Console")}
+                  onClick={() => setLogOpen(true)}
+                >
+                  <FeaturedPlayListRounded fontSize="inherit" />
+                </IconButton>
+              </Badge>
+            ) : (
+              <IconButton
+                size="small"
+                edge="start"
+                color="inherit"
+                title={t("Script Console")}
+                onClick={() => setLogOpen(true)}
+              >
+                <FeaturedPlayListRounded fontSize="inherit" />
+              </IconButton>
+            )
+          ) : (
+            <Typography
+              noWrap
+              title={t(`${id} Description`)}
+              sx={i18n.language === "zh" ? { width: "calc(100% - 75px)" } : {}}
+            >
+              {t(`${id} Description`)}
+            </Typography>
+          )}
+        </Box>
+      </ProfileBox>
+
+      <Menu
+        open={!!anchorEl}
+        anchorEl={anchorEl}
+        onClose={() => setAnchorEl(null)}
+        anchorPosition={position}
+        anchorReference="anchorPosition"
+        transitionDuration={225}
+        MenuListProps={{ sx: { py: 0.5 } }}
+        onContextMenu={(e) => {
+          setAnchorEl(null);
+          e.preventDefault();
+        }}
+      >
+        {itemMenu
+          .filter((item: any) => item.show !== false)
+          .map((item) => (
+            <MenuItem
+              key={item.label}
+              onClick={item.handler}
+              sx={[
+                { minWidth: 120 },
+                (theme) => {
+                  return {
+                    color:
+                      item.label === "Delete"
+                        ? theme.palette.error.main
+                        : undefined,
+                  };
+                },
+              ]}
+              dense
+            >
+              {t(item.label)}
+            </MenuItem>
+          ))}
+      </Menu>
+
+      <EditorViewer
+        mode="profile"
+        property={id}
+        open={fileOpen}
+        language={id === "Merge" ? "yaml" : "javascript"}
+        schema={id === "Merge" ? "merge" : undefined}
+        onChange={onChange}
+        onClose={() => setFileOpen(false)}
+      />
+
+      <LogViewer
+        open={logOpen}
+        logInfo={logInfo}
+        onClose={() => setLogOpen(false)}
+      />
+    </>
+  );
+};

+ 39 - 0
src/pages/profiles.tsx

@@ -41,6 +41,7 @@ import {
   ProfileViewer,
   ProfileViewer,
   ProfileViewerRef,
   ProfileViewerRef,
 } from "@/components/profile/profile-viewer";
 } from "@/components/profile/profile-viewer";
+import { ProfileMore } from "@/components/profile/profile-more";
 import { ProfileItem } from "@/components/profile/profile-item";
 import { ProfileItem } from "@/components/profile/profile-item";
 import { useProfiles } from "@/hooks/use-profiles";
 import { useProfiles } from "@/hooks/use-profiles";
 import { ConfigViewer } from "@/components/setting/mods/config-viewer";
 import { ConfigViewer } from "@/components/setting/mods/config-viewer";
@@ -49,6 +50,7 @@ import { BaseStyledTextField } from "@/components/base/base-styled-text-field";
 import { listen } from "@tauri-apps/api/event";
 import { listen } from "@tauri-apps/api/event";
 import { readTextFile } from "@tauri-apps/api/fs";
 import { readTextFile } from "@tauri-apps/api/fs";
 import { readText } from "@tauri-apps/api/clipboard";
 import { readText } from "@tauri-apps/api/clipboard";
+import { EditorViewer } from "@/components/profile/editor-viewer";
 
 
 const ProfilePage = () => {
 const ProfilePage = () => {
   const { t } = useTranslation();
   const { t } = useTranslation();
@@ -244,6 +246,12 @@ const ProfilePage = () => {
     if (text) setUrl(text);
     if (text) setUrl(text);
   };
   };
 
 
+  const mode = useThemeMode();
+  const islight = mode === "light" ? true : false;
+  const dividercolor = islight
+    ? "rgba(0, 0, 0, 0.06)"
+    : "rgba(255, 255, 255, 0.06)";
+
   return (
   return (
     <BasePage
     <BasePage
       full
       full
@@ -383,7 +391,38 @@ const ProfilePage = () => {
             </Grid>
             </Grid>
           </Box>
           </Box>
         </DndContext>
         </DndContext>
+        <Divider
+          variant="middle"
+          flexItem
+          sx={{ width: `calc(100% - 32px)`, borderColor: dividercolor }}
+        ></Divider>
+        <Box sx={{ mt: 1.5 }}>
+          <Grid container spacing={{ xs: 1, lg: 1 }}>
+            <Grid item sm={6} md={6} lg={6}>
+              <ProfileMore
+                id="Merge"
+                onChange={async (prev, curr) => {
+                  if (prev !== curr) {
+                    await onEnhance();
+                  }
+                }}
+              />
+            </Grid>
+            <Grid item sm={6} md={6} lg={6}>
+              <ProfileMore
+                id="Script"
+                logInfo={chainLogs["Script"]}
+                onChange={async (prev, curr) => {
+                  if (prev !== curr) {
+                    await onEnhance();
+                  }
+                }}
+              />
+            </Grid>
+          </Grid>
+        </Box>
       </Box>
       </Box>
+
       <ProfileViewer ref={viewerRef} onChange={() => mutateProfiles()} />
       <ProfileViewer ref={viewerRef} onChange={() => mutateProfiles()} />
       <ConfigViewer ref={configRef} />
       <ConfigViewer ref={configRef} />
     </BasePage>
     </BasePage>