From c53fe0ed1f983fd9b01f02c022c34464b3fd3eec Mon Sep 17 00:00:00 2001
From: GyDi <segydi@foxmail.com>
Date: Mon, 7 Feb 2022 17:26:05 +0800
Subject: [PATCH] feat: support new profile

---
 src-tauri/src/cmds.rs          | 36 ++++++++++++++++++++++++++
 src-tauri/src/core/profiles.rs | 47 ++++++++++++++++++++++++++++++++++
 src-tauri/src/main.rs          |  1 +
 src/pages/profiles.tsx         | 22 +++++++++++++++-
 src/services/cmds.ts           |  4 +++
 5 files changed, 109 insertions(+), 1 deletion(-)

diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs
index 8f54fb3..8cf1b54 100644
--- a/src-tauri/src/cmds.rs
+++ b/src-tauri/src/cmds.rs
@@ -43,6 +43,42 @@ pub async fn import_profile(
   }
 }
 
+/// new a profile
+/// append a temp profile item file to the `profiles` dir
+/// view the temp profile file by using vscode or other editor
+#[tauri::command]
+pub async fn new_profile(
+  name: String,
+  desc: String,
+  profiles_state: State<'_, ProfilesState>,
+) -> Result<(), String> {
+  let mut profiles = profiles_state.0.lock().unwrap();
+
+  let (_, path) = profiles.append_item(name, desc)?;
+
+  if !path.exists() {
+    return Err("the file not found".into());
+  }
+
+  // use vscode first
+  if let Ok(code) = which::which("code") {
+    return match Command::new(code).arg(path).status() {
+      Ok(_) => Ok(()),
+      Err(_) => Err("failed to open file by VScode".into()),
+    };
+  }
+
+  // use `open` command
+  if let Ok(open) = which::which("open") {
+    return match Command::new(open).arg(path).status() {
+      Ok(_) => Ok(()),
+      Err(_) => Err("failed to open file by `open`".into()),
+    };
+  }
+
+  return Err("failed to open the file, please edit the file manually".into());
+}
+
 /// Update the profile
 #[tauri::command]
 pub async fn update_profile(
diff --git a/src-tauri/src/core/profiles.rs b/src-tauri/src/core/profiles.rs
index 96093c7..3f30ffa 100644
--- a/src-tauri/src/core/profiles.rs
+++ b/src-tauri/src/core/profiles.rs
@@ -7,6 +7,7 @@ use std::collections::HashMap;
 use std::env::temp_dir;
 use std::fs::File;
 use std::io::Write;
+use std::path::PathBuf;
 use std::time::{SystemTime, UNIX_EPOCH};
 
 /// Define the `profiles.yaml` schema
@@ -23,6 +24,8 @@ pub struct Profiles {
 pub struct ProfileItem {
   /// profile name
   pub name: Option<String>,
+  /// profile description
+  pub desc: Option<String>,
   /// profile file
   pub file: Option<String>,
   /// current mode
@@ -109,6 +112,7 @@ impl Profiles {
 
     items.push(ProfileItem {
       name: Some(result.name),
+      desc: Some("imported url".into()),
       file: Some(result.file),
       mode: Some(format!("rule")),
       url: Some(url),
@@ -138,6 +142,49 @@ impl Profiles {
     self.save_file()
   }
 
+  /// append new item
+  /// return the new item's index
+  pub fn append_item(&mut self, name: String, desc: String) -> Result<(usize, PathBuf), String> {
+    let mut items = self.items.take().unwrap_or(vec![]);
+
+    // create a new profile file
+    let now = SystemTime::now()
+      .duration_since(UNIX_EPOCH)
+      .unwrap()
+      .as_secs();
+    let file = format!("{}.yaml", now);
+    let path = dirs::app_home_dir().join("profiles").join(&file);
+
+    let file_data = b"# Profile Template for clash verge\n
+# proxies defination (optional, the same as clash)
+proxies:\n
+# proxy-groups (optional, the same as clash)
+proxy-groups:\n
+# rules (optional, the same as clash)
+rules:\n\n
+";
+
+    match File::create(&path).unwrap().write(file_data) {
+      Ok(_) => {
+        items.push(ProfileItem {
+          name: Some(name),
+          desc: Some(desc),
+          file: Some(file),
+          mode: None,
+          url: None,
+          selected: Some(vec![]),
+          extra: None,
+          updated: Some(now as usize),
+        });
+
+        let index = items.len();
+        self.items = Some(items);
+        Ok((index, path))
+      }
+      Err(_) => Err("failed to create file".into()),
+    }
+  }
+
   /// update the target profile
   /// and save to config file
   /// only support the url item
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
index ba743a3..3184c87 100644
--- a/src-tauri/src/main.rs
+++ b/src-tauri/src/main.rs
@@ -81,6 +81,7 @@ fn main() -> std::io::Result<()> {
       cmds::get_verge_config,
       cmds::patch_verge_config,
       // profile
+      cmds::new_profile,
       cmds::view_profile,
       cmds::patch_profile,
       cmds::import_profile,
diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx
index 75a2639..299658a 100644
--- a/src/pages/profiles.tsx
+++ b/src/pages/profiles.tsx
@@ -6,6 +6,7 @@ import {
   selectProfile,
   patchProfile,
   importProfile,
+  newProfile,
 } from "../services/cmds";
 import { getProxies, updateProxy } from "../services/api";
 import noop from "../utils/noop";
@@ -94,6 +95,21 @@ const ProfilePage = () => {
     }
   };
 
+  const lockNewRef = useRef(false);
+  const onNew = async () => {
+    if (lockNewRef.current) return;
+    lockNewRef.current = true;
+
+    try {
+      await newProfile("New Profile", "no desc");
+      mutate("getProfiles");
+    } catch (err: any) {
+      err && Notice.error(err.toString());
+    } finally {
+      lockNewRef.current = false;
+    }
+  };
+
   return (
     <BasePage title="Profiles">
       <Box sx={{ display: "flex", mb: 3 }}>
@@ -105,15 +121,19 @@ const ProfilePage = () => {
           fullWidth
           value={url}
           onChange={(e) => setUrl(e.target.value)}
-          sx={{ mr: 2 }}
+          sx={{ mr: 1 }}
         />
         <Button
           disabled={!url || disabled}
           variant="contained"
           onClick={onImport}
+          sx={{ mr: 1 }}
         >
           Import
         </Button>
+        <Button variant="contained" onClick={onNew}>
+          New
+        </Button>
       </Box>
 
       <Grid container spacing={3}>
diff --git a/src/services/cmds.ts b/src/services/cmds.ts
index 6f5703f..5f9cf08 100644
--- a/src/services/cmds.ts
+++ b/src/services/cmds.ts
@@ -9,6 +9,10 @@ export async function syncProfiles() {
   return invoke<void>("sync_profiles");
 }
 
+export async function newProfile(name: string, desc: string) {
+  return invoke<void>("new_profile", { name, desc });
+}
+
 export async function viewProfile(index: number) {
   return invoke<void>("view_profile", { index });
 }
-- 
GitLab