From 462f70f35d5565c245c4fc2837b30667f5879996 Mon Sep 17 00:00:00 2001 From: selcarpa Date: Tue, 21 Oct 2025 15:37:04 +0800 Subject: [PATCH] feat(core): Implements core configuration file generation functionality - Adds the bootArgs.ts utility module for generating and deleting configuration files - Integrates configuration file generation and cleanup logic into spary.vue - Integrates the tauri-plugin-fs plugin to support file system operations - Adds a Linux system proxy configuration script --- .gitignore | 1 + package.json | 1 + scripts/set_linux_proxy.sh | 159 ++++++++++++++ src-tauri/Cargo.toml | 1 + src-tauri/capabilities/default.json | 19 +- src-tauri/src/cores.rs | 3 + src-tauri/src/exe.rs | 130 +++++++++++ src-tauri/src/lib.rs | 9 + src/App.vue | 53 +---- src/components.d.ts | 1 + src/components/index/mainDrawer.vue | 60 ++++++ src/components/index/spary.vue | 18 +- src/locales/en.json | 1 + src/locales/zh.json | 1 + src/plugins/i18n.ts | 2 +- src/utils/bootArgs.ts | 321 ++++++++++++++++++++++++++++ yarn.lock | 7 + 17 files changed, 731 insertions(+), 56 deletions(-) create mode 100644 scripts/set_linux_proxy.sh create mode 100644 src-tauri/src/cores.rs create mode 100644 src-tauri/src/exe.rs create mode 100644 src/components/index/mainDrawer.vue create mode 100644 src/utils/bootArgs.ts diff --git a/.gitignore b/.gitignore index eb50757..36c9a1a 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,4 @@ Desktop.ini coverage/ *.lcov .junit/ +/cores/ diff --git a/package.json b/package.json index 55498e6..4082e99 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@fontsource/roboto": "5.2.7", "@mdi/font": "7.4.47", "@tauri-apps/api": "^2", + "@tauri-apps/plugin-fs": "~2", "@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-sql": "~2", "vue": "^3.5.21", diff --git a/scripts/set_linux_proxy.sh b/scripts/set_linux_proxy.sh new file mode 100644 index 0000000..11b6b6a --- /dev/null +++ b/scripts/set_linux_proxy.sh @@ -0,0 +1,159 @@ +#!/bin/bash + +# fork from https://github.com/2dust/v2rayN/blob/899b3fc97bc5024cd514bdbc8626a5d3c98170a7/v2rayN/ServiceLib/Sample/proxy_set_linux_sh + +# Function to set proxy for GNOME +set_gnome_proxy() { + local MODE=$1 + local PROXY_IP=$2 + local PROXY_PORT=$3 + local IGNORE_HOSTS=$4 + + # Set the proxy mode + gsettings set org.gnome.system.proxy mode "$MODE" + + if [ "$MODE" == "manual" ]; then + # List of protocols + local PROTOCOLS=("http" "https" "ftp" "socks") + + # Loop through protocols to set the proxy + for PROTOCOL in "${PROTOCOLS[@]}"; do + gsettings set org.gnome.system.proxy.$PROTOCOL host "$PROXY_IP" + gsettings set org.gnome.system.proxy.$PROTOCOL port "$PROXY_PORT" + done + + # Set ignored hosts + gsettings set org.gnome.system.proxy ignore-hosts "['$IGNORE_HOSTS']" + + echo "GNOME: Manual proxy settings applied." + echo "Proxy IP: $PROXY_IP" + echo "Proxy Port: $PROXY_PORT" + echo "Ignored Hosts: $IGNORE_HOSTS" + elif [ "$MODE" == "none" ]; then + echo "GNOME: Proxy disabled." + fi +} + +# Function to set proxy for KDE +set_kde_proxy() { + local MODE=$1 + local PROXY_IP=$2 + local PROXY_PORT=$3 + local IGNORE_HOSTS=$4 + + # Determine the correct kwriteconfig command based on KDE_SESSION_VERSION + if [ "$KDE_SESSION_VERSION" == "6" ]; then + KWRITECONFIG="kwriteconfig6" + else + KWRITECONFIG="kwriteconfig5" + fi + + # KDE uses kwriteconfig to modify proxy settings + if [ "$MODE" == "manual" ]; then + # Set proxy for all protocols + $KWRITECONFIG --file kioslaverc --group "Proxy Settings" --key ProxyType 1 + $KWRITECONFIG --file kioslaverc --group "Proxy Settings" --key httpProxy "http://$PROXY_IP:$PROXY_PORT" + $KWRITECONFIG --file kioslaverc --group "Proxy Settings" --key httpsProxy "http://$PROXY_IP:$PROXY_PORT" + $KWRITECONFIG --file kioslaverc --group "Proxy Settings" --key ftpProxy "http://$PROXY_IP:$PROXY_PORT" + $KWRITECONFIG --file kioslaverc --group "Proxy Settings" --key socksProxy "http://$PROXY_IP:$PROXY_PORT" + + # Set ignored hosts + $KWRITECONFIG --file kioslaverc --group "Proxy Settings" --key NoProxyFor "$IGNORE_HOSTS" + + echo "KDE: Manual proxy settings applied." + echo "Proxy IP: $PROXY_IP" + echo "Proxy Port: $PROXY_PORT" + echo "Ignored Hosts: $IGNORE_HOSTS" + elif [ "$MODE" == "none" ]; then + # Disable proxy + $KWRITECONFIG --file kioslaverc --group "Proxy Settings" --key ProxyType 0 + echo "KDE: Proxy disabled." + fi + + # Apply changes by restarting KDE's network settings + dbus-send --type=signal /KIO/Scheduler org.kde.KIO.Scheduler.reparseSlaveConfiguration string:"" +} + +# Detect the current desktop environment +detect_desktop_environment() { + if [[ "$XDG_CURRENT_DESKTOP" == *"GNOME"* ]] || [[ "$XDG_SESSION_DESKTOP" == *"GNOME"* ]]; then + echo "gnome" + return + fi + + if [[ "$XDG_CURRENT_DESKTOP" == *"XFCE"* ]] || [[ "$XDG_SESSION_DESKTOP" == *"XFCE"* ]]; then + echo "gnome" + return + fi + + if [[ "$XDG_CURRENT_DESKTOP" == *"X-Cinnamon"* ]] || [[ "$XDG_SESSION_DESKTOP" == *"cinnamon"* ]]; then + echo "gnome" + return + fi + + if [[ "$XDG_CURRENT_DESKTOP" == *"UKUI"* ]] || [[ "$XDG_SESSION_DESKTOP" == *"ukui"* ]]; then + echo "gnome" + return + fi + + if [[ "$XDG_CURRENT_DESKTOP" == *"DDE"* ]] || [[ "$XDG_SESSION_DESKTOP" == *"dde"* ]]; then + echo "gnome" + return + fi + + if [[ "$XDG_CURRENT_DESKTOP" == *"MATE"* ]] || [[ "$XDG_SESSION_DESKTOP" == *"mate"* ]]; then + echo "gnome" + return + fi + + local KDE_ENVIRONMENTS=("KDE" "plasma") + for ENV in "${KDE_ENVIRONMENTS[@]}"; do + if [ "$XDG_CURRENT_DESKTOP" == "$ENV" ] || [ "$XDG_SESSION_DESKTOP" == "$ENV" ]; then + echo "kde" + return + fi + done + + # Fallback to GNOME method if CLI utility is available. This solves the + # proxy configuration issues on minimal installation systems, like setups + # with only window managers, that borrow some parts from big DEs. + if command -v gsettings >/dev/null 2>&1; then + echo "gnome" + return + fi + + echo "unsupported" +} + +# Main script logic +if [ "$#" -lt 1 ]; then + echo "Usage: $0 [proxy_ip proxy_port ignore_hosts]" + echo " mode: 'none' or 'manual'" + echo " If mode is 'manual', provide proxy IP, port, and ignore hosts." + exit 1 +fi + +# Get the mode +MODE=$1 +PROXY_IP=$2 +PROXY_PORT=$3 +IGNORE_HOSTS=$4 + +if ! [[ "$MODE" =~ ^(manual|none)$ ]]; then + echo "Invalid mode. Use 'none' or 'manual'." >&2 + exit 1 +fi + +# Detect desktop environment +DE=$(detect_desktop_environment) + +# Apply settings based on the desktop environment +if [ "$DE" == "gnome" ]; then + set_gnome_proxy "$MODE" "$PROXY_IP" "$PROXY_PORT" "$IGNORE_HOSTS" +elif [ "$DE" == "kde" ]; then + set_gnome_proxy "$MODE" "$PROXY_IP" "$PROXY_PORT" "$IGNORE_HOSTS" + set_kde_proxy "$MODE" "$PROXY_IP" "$PROXY_PORT" "$IGNORE_HOSTS" +else + echo "Unsupported desktop environment: $DE" >&2 + exit 1 +fi \ No newline at end of file diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index fbad824..d1888c1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -23,3 +23,4 @@ tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" tauri-plugin-sql = { version = "2", features = ["sqlite"] } +tauri-plugin-fs = "2" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 2ade1f0..7e75c02 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -9,6 +9,23 @@ "core:default", "opener:default", "sql:default", - "sql:allow-execute" + "sql:allow-execute", + "fs:default", + { + "identifier": "fs:allow-write-text-file", + "allow": [ + { + "path": "$APPCONFIG/*" + } + ] + }, + { + "identifier": "fs:allow-remove", + "allow": [ + { + "path": "$APPCONFIG/*" + } + ] + } ] } \ No newline at end of file diff --git a/src-tauri/src/cores.rs b/src-tauri/src/cores.rs new file mode 100644 index 0000000..d0b7509 --- /dev/null +++ b/src-tauri/src/cores.rs @@ -0,0 +1,3 @@ +enum Core{ + XRAY(String), +} \ No newline at end of file diff --git a/src-tauri/src/exe.rs b/src-tauri/src/exe.rs new file mode 100644 index 0000000..3ac0d79 --- /dev/null +++ b/src-tauri/src/exe.rs @@ -0,0 +1,130 @@ +use std::io::{BufRead, BufReader}; +use std::path::PathBuf; +use std::process::{Child, Command, Stdio}; +use std::sync::{Arc, Mutex}; +use std::thread; + +pub struct Exe { + path: PathBuf, + args: Vec, + child: Arc>>, + pub pid: Arc>>, +} + +impl Exe { + pub fn new, S: Into>(path: P, args: Vec) -> Self { + Self { + path: path.into(), + args: args.into_iter().map(|s| s.into()).collect(), + child: Arc::new(Mutex::new(None)), + pid: Arc::new(Mutex::new(None)), + } + } + + pub fn start( + &mut self, + mut on_stdout: F, + mut on_stderr: E, + mut on_exit: X, + ) -> std::io::Result<()> + where + F: FnMut(String) + Send + 'static, + E: FnMut(String) + Send + 'static, + X: FnMut(i32) + Send + 'static, + { + let mut cmd = Command::new(&self.path); + cmd.args(&self.args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + let mut child = cmd.spawn()?; + + // ✅ 保存 PID + let pid = child.id(); + { + *self.pid.lock().unwrap() = Some(pid); + } + + // ✅ 异步读取 stdout + if let Some(out) = child.stdout.take() { + let mut reader = BufReader::new(out); + let mut line = String::new(); + thread::spawn(move || { + while let Ok(n) = reader.read_line(&mut line) { + if n == 0 { + break; + } + on_stdout(line.clone()); + line.clear(); + } + }); + } + + if let Some(err) = child.stderr.take() { + let mut reader = BufReader::new(err); + let mut line = String::new(); + thread::spawn(move || { + while let Ok(n) = reader.read_line(&mut line) { + if n == 0 { + break; + } + on_stderr(line.clone()); + line.clear(); + } + }); + } + + let child_arc = self.child.clone(); + *child_arc.lock().unwrap() = Some(child); + + let pid_clone = self.pid.clone(); + thread::spawn(move || { + if let Some(mut c) = child_arc.lock().unwrap().take() { + match c.wait() { + Ok(status) => { + let code = status.code().unwrap_or(-1); + *pid_clone.lock().unwrap() = None; + on_exit(code); + } + Err(e) => { + eprintln!("Error waiting for process: {e}"); + *pid_clone.lock().unwrap() = None; + on_exit(-1); + } + } + } + }); + + Ok(()) + } + + pub fn kill(&self) -> std::io::Result<()> { + let mut guard = self.child.lock().unwrap(); + if let Some(child) = guard.as_mut() { + child.kill()?; + Ok(()) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "No running process", + )) + } + } + + pub fn is_alive(&self) -> bool { + let mut guard = self.child.lock().unwrap(); + if let Some(child) = guard.as_mut() { + match child.try_wait() { + Ok(Some(_)) => false, // 已退出 + Ok(None) => true, // 仍在运行 + Err(_) => false, + } + } else { + false + } + } + + pub fn get_pid(&self) -> Option { + *self.pid.lock().unwrap() + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c961cbf..706aa3f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -3,8 +3,10 @@ use crate::group::add_group; use crate::spary::spary_switch; use tauri_plugin_sql::{Migration, MigrationKind}; +mod exe; mod group; mod spary; +mod cores; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { @@ -21,8 +23,15 @@ pub fn run() { sql: "CREATE TABLE node(id INTEGER PRIMARY KEY AUTOINCREMENT, alias VARCHAR(60) NOT NULL,arguments JSON NOT NULL default '{}', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, group_id INTEGER NOT NULL)", kind: MigrationKind::Up, }, + Migration{ + version:3, + description:"add default group", + sql:"INSERT INTO `group`(name) VALUES('default')", + kind:MigrationKind::Up, + } ]; tauri::Builder::default() + .plugin(tauri_plugin_fs::init()) .plugin( tauri_plugin_sql::Builder::default() .add_migrations("sqlite:spary.db", migrations) diff --git a/src/App.vue b/src/App.vue index 015c40a..a5a7bc2 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,51 +1,10 @@ diff --git a/src/components.d.ts b/src/components.d.ts index 6fb335c..f187ea9 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -11,6 +11,7 @@ declare module 'vue' { AddGroup: typeof import('./components/nodeEdit/addGroup.vue')['default'] AddNode: typeof import('./components/nodeEdit/addNode.vue')['default'] LanguageSwitcher: typeof import('./components/LanguageSwitcher.vue')['default'] + MainDrawer: typeof import('./components/index/mainDrawer.vue')['default'] NodeList: typeof import('./components/nodeEdit/nodeList.vue')['default'] NodesFloatButton: typeof import('./components/nodeEdit/nodesFloatButton.vue')['default'] NotificationProvider: typeof import('./components/notify/notificationProvider.vue')['default'] diff --git a/src/components/index/mainDrawer.vue b/src/components/index/mainDrawer.vue new file mode 100644 index 0000000..9ebae0a --- /dev/null +++ b/src/components/index/mainDrawer.vue @@ -0,0 +1,60 @@ + + + + + \ No newline at end of file diff --git a/src/components/index/spary.vue b/src/components/index/spary.vue index 27932c1..11a46be 100644 --- a/src/components/index/spary.vue +++ b/src/components/index/spary.vue @@ -2,9 +2,9 @@
@@ -13,14 +13,22 @@ diff --git a/src/locales/en.json b/src/locales/en.json index 2835cb6..f4c11c0 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1,5 +1,6 @@ { "app": { + "mainPage": "Home", "title": "Spary", "nodes": "Nodes", "settings": "Settings", diff --git a/src/locales/zh.json b/src/locales/zh.json index 3a5610d..c8712d8 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -1,5 +1,6 @@ { "app": { + "mainPage": "主页", "title": "Spary", "nodes": "节点", "settings": "设置", diff --git a/src/plugins/i18n.ts b/src/plugins/i18n.ts index df8f7a9..ee381a6 100644 --- a/src/plugins/i18n.ts +++ b/src/plugins/i18n.ts @@ -10,7 +10,7 @@ const messages = { const i18n = createI18n({ legacy: false, // Use composition API mode locale: 'en', // Default locale - fallbackLocale: 'en', // Fallback locale + fallbackLocale: 'zh', // Fallback locale messages }) diff --git a/src/utils/bootArgs.ts b/src/utils/bootArgs.ts new file mode 100644 index 0000000..a33c2d7 --- /dev/null +++ b/src/utils/bootArgs.ts @@ -0,0 +1,321 @@ +import {writeTextFile, BaseDirectory, remove} from '@tauri-apps/plugin-fs'; +export async function removeConfigFile(){ + try { + await remove('config.json', { baseDir: BaseDirectory.AppConfig }); + } catch (error) { + // ignore + console.warn(error); + } +} +export async function genConfigFile() { + await removeConfigFile() + const configContent = ` + { + "log": { + "loglevel": "warning" + }, + "dns": { + "hosts": { + "dns.google": [ + "8.8.8.8", + "8.8.4.4", + "2001:4860:4860::8888", + "2001:4860:4860::8844" + ], + "dns.alidns.com": [ + "223.5.5.5", + "223.6.6.6", + "2400:3200::1", + "2400:3200:baba::1" + ], + "one.one.one.one": [ + "1.1.1.1", + "1.0.0.1", + "2606:4700:4700::1111", + "2606:4700:4700::1001" + ], + "1dot1dot1dot1.cloudflare-dns.com": [ + "1.1.1.1", + "1.0.0.1", + "2606:4700:4700::1111", + "2606:4700:4700::1001" + ], + "cloudflare-dns.com": [ + "104.16.249.249", + "104.16.248.249", + "2606:4700::6810:f8f9", + "2606:4700::6810:f9f9" + ], + "dns.cloudflare.com": [ + "104.16.132.229", + "104.16.133.229", + "2606:4700::6810:84e5", + "2606:4700::6810:85e5" + ], + "dot.pub": [ + "1.12.12.12", + "120.53.53.53" + ], + "doh.pub": [ + "1.12.12.12", + "120.53.53.53" + ], + "dns.quad9.net": [ + "9.9.9.9", + "149.112.112.112", + "2620:fe::fe", + "2620:fe::9" + ], + "dns.yandex.net": [ + "77.88.8.8", + "77.88.8.1", + "2a02:6b8::feed:0ff", + "2a02:6b8:0:1::feed:0ff" + ], + "dns.sb": [ + "185.222.222.222", + "2a09::" + ], + "dns.umbrella.com": [ + "208.67.220.220", + "208.67.222.222", + "2620:119:35::35", + "2620:119:53::53" + ], + "dns.sse.cisco.com": [ + "208.67.220.220", + "208.67.222.222", + "2620:119:35::35", + "2620:119:53::53" + ], + "engage.cloudflareclient.com": [ + "162.159.192.1", + "2606:4700:d0::a29f:c001" + ] + }, + "servers": [ + { + "address": "https://cloudflare-dns.com/dns-query", + "domains": [ + "domain:googleapis.cn", + "domain:gstatic.com" + ], + "skipFallback": true + }, + { + "address": "https://dns.alidns.com/dns-query", + "domains": [ + "domain:alidns.com", + "domain:doh.pub", + "domain:dot.pub", + "domain:360.cn", + "domain:onedns.net", + "pioneer.kakakuai.top" + ], + "skipFallback": true + }, + { + "address": "https://dns.alidns.com/dns-query", + "domains": [ + "geosite:private", + "geosite:cn" + ], + "skipFallback": true + }, + "https://cloudflare-dns.com/dns-query" + ] + }, + "inbounds": [ + { + "tag": "socks", + "port": 10808, + "listen": "0.0.0.0", + "protocol": "mixed", + "sniffing": { + "enabled": true, + "destOverride": [ + "http", + "tls" + ], + "routeOnly": false + }, + "settings": { + "auth": "noauth", + "udp": true, + "allowTransparent": false + } + }, + { + "tag": "api", + "port": 10812, + "listen": "127.0.0.1", + "protocol": "dokodemo-door", + "settings": { + "address": "127.0.0.1" + } + } + ], + "outbounds": [ + { + "tag": "proxy", + "protocol": "trojan", + "settings": { + "servers": [ + { + "address": "pioneer.kakakuai.top", + "method": "", + "ota": false, + "password": "0FUz1P7obM", + "port": 443, + "level": 1 + } + ] + }, + "streamSettings": { + "network": "ws", + "security": "tls", + "tlsSettings": { + "allowInsecure": false + }, + "wsSettings": { + "path": "/dsf", + "headers": {} + } + }, + "mux": { + "enabled": false, + "concurrency": -1 + } + }, + { + "tag": "direct", + "protocol": "freedom" + }, + { + "tag": "block", + "protocol": "blackhole" + } + ], + "routing": { + "domainStrategy": "AsIs", + "rules": [ + { + "type": "field", + "inboundTag": [ + "api" + ], + "outboundTag": "api" + }, + { + "type": "field", + "outboundTag": "proxy", + "domain": [ + "domain:googleapis.cn", + "domain:gstatic.com" + ] + }, + { + "type": "field", + "port": "443", + "network": "udp", + "outboundTag": "block" + }, + { + "type": "field", + "outboundTag": "direct", + "ip": [ + "geoip:private" + ] + }, + { + "type": "field", + "outboundTag": "direct", + "domain": [ + "geosite:private" + ] + }, + { + "type": "field", + "outboundTag": "direct", + "ip": [ + "223.5.5.5", + "223.6.6.6", + "2400:3200::1", + "2400:3200:baba::1", + "119.29.29.29", + "1.12.12.12", + "120.53.53.53", + "2402:4e00::", + "2402:4e00:1::", + "180.76.76.76", + "2400:da00::6666", + "114.114.114.114", + "114.114.115.115", + "114.114.114.119", + "114.114.115.119", + "114.114.114.110", + "114.114.115.110", + "180.184.1.1", + "180.184.2.2", + "101.226.4.6", + "218.30.118.6", + "123.125.81.6", + "140.207.198.6", + "1.2.4.8", + "210.2.4.8", + "52.80.66.66", + "117.50.22.22", + "2400:7fc0:849e:200::4", + "2404:c2c0:85d8:901::4", + "117.50.10.10", + "52.80.52.52", + "2400:7fc0:849e:200::8", + "2404:c2c0:85d8:901::8", + "117.50.60.30", + "52.80.60.30" + ] + }, + { + "type": "field", + "outboundTag": "direct", + "domain": [ + "domain:alidns.com", + "domain:doh.pub", + "domain:dot.pub", + "domain:360.cn", + "domain:onedns.net" + ] + }, + { + "type": "field", + "outboundTag": "direct", + "ip": [ + "geoip:cn" + ] + }, + { + "type": "field", + "outboundTag": "direct", + "domain": [ + "geosite:cn" + ] + } + ] + }, + "metrics": { + "tag": "api" + }, + "policy": { + "system": { + "statsOutboundUplink": true, + "statsOutboundDownlink": true + } + }, + "stats": {} + } + ` + const finalContent = JSON.stringify(JSON.parse(configContent), null, 4); + await writeTextFile('config.json', finalContent, { + baseDir: BaseDirectory.AppConfig, + }); +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index ae9e541..e373a25 100644 --- a/yarn.lock +++ b/yarn.lock @@ -683,6 +683,13 @@ "@tauri-apps/cli-win32-ia32-msvc" "2.8.4" "@tauri-apps/cli-win32-x64-msvc" "2.8.4" +"@tauri-apps/plugin-fs@~2": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-fs/-/plugin-fs-2.4.2.tgz#cce4ea16dc3614f7a476988457ec4cf0024d81f6" + integrity sha512-YGhmYuTgXGsi6AjoV+5mh2NvicgWBfVJHHheuck6oHD+HC9bVWPaHvCP0/Aw4pHDejwrvT8hE3+zZAaWf+hrig== + dependencies: + "@tauri-apps/api" "^2.8.0" + "@tauri-apps/plugin-opener@^2": version "2.5.0" resolved "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.0.tgz"