Merge branch 'pr/20' into dev-tauri2.0

This commit is contained in:
VirtualHotBar
2024-06-02 17:29:00 +08:00
23 changed files with 592 additions and 1090 deletions

3
.gitignore vendored
View File

@@ -25,3 +25,6 @@ dist-ssr
# bin
src-tauri/res/bin
# TODO: .
scripts

685
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,14 @@
[package]
name = "app"
description = "NetMount"
version = "1.0.6"
version = "1.1.0"
authors = ["VirtualHotBar"]
license = ""
license = "AGPL-3.0"
repository = ""
default-run = "app"
edition = "2021"
rust-version = "1.60"
windows_subsystem = "windows"
[package.metadata.windows]
manifest = "app.manifest"
@@ -40,37 +38,21 @@ tauri = { version = "2.0.0-beta", features = [
] }
anyhow = "1"
anyhow-tauri = "1"
phf = { version = "0", features = ["macros", "serde"] }
phf = { version = "0.11", features = ["macros", "serde"] }
rand = "0.8"
directories = "5.0.1"
reqwest = { version = "0.11", features = ["json", "stream"] }
tokio = { version = "1", features = ["full"] }
futures-util = "0.3"
sysinfo = "0.30.10"
once_cell = "1.19.0"
lazy_static = "1.4.0"
fslock = "0.2.1"
# ipc-channel = "0.18.0"
rfd = "0.14.1"
tauri-plugin-shell = "2.0.0-beta.7"
tauri-plugin-os = "2.0.0-beta.6"
tauri-plugin-fs = "2.0.0-beta.9"
tauri-plugin-process = "2.0.0-beta.6"
raw-window-handle = "0.6"
# tauri-plugin-autostart = "2.0.0-beta.7"
tauri-plugin-single-instance = "2.0.0-beta.9"
[target.'cfg(windows)'.dependencies]
winreg = "0.10.1"
winapi = "0.3"
widestring = "1.1"
window-shadows = "0.2.2"
[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
tauri-plugin-single-instance = "2.0.0-beta.9"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.

View File

@@ -1,4 +1,3 @@
use std::env::current_dir;
#[cfg(not(target_os = "windows"))]
use std::os::unix::fs::PermissionsExt;
use std::process::exit;
@@ -15,11 +14,14 @@ struct ResBinUrls {
fn main() -> anyhow::Result<()> {
check_res_bin();
compile_locale(&[
("en", Path::new("locales/en.json")),
("zh-cn", Path::new("locales/zh-cn.json")),
("zh-hant", Path::new("locales/zh-hant.json")),
])?;
compile_locale(
&[
("en", Path::new("locales/en.json")),
("zh-cn", Path::new("locales/zh-cn.json")),
("zh-hant", Path::new("locales/zh-hant.json")),
],
"zh-cn",
)?;
tauri_build::try_build(Attributes::default())?;
Ok(())
}
@@ -29,9 +31,9 @@ fn escape(str: &str) -> String {
format!("r{bound}\"{str}\"{bound}")
}
fn compile_locale(locales: &[(&str, &Path)]) -> anyhow::Result<()> {
fn compile_locale(locales: &[(&str, &Path)], default: &str) -> anyhow::Result<()> {
let mut file =
File::create(Path::new(&env::var("OUT_DIR").unwrap()).join(format!("codegen.rs")))?;
File::create(Path::new(&env::var("OUT_DIR").unwrap()).join(format!("language.rs")))?;
write!(&mut file, "type Pack=phf::Map<&'static str,&'static str>;")?;
let get_name = |name: &str| format!("LANG_{}", name.replace("-", "_").to_uppercase());
@@ -48,11 +50,12 @@ fn compile_locale(locales: &[(&str, &Path)]) -> anyhow::Result<()> {
}
write!(
&mut file,
"fn get_lang(name:&'static str)->Pack{{match name{{{}}}}}",
"fn get_lang(name:&str)->&'static Pack{{match name{{{}_=>&{}}}}}",
locales
.iter()
.map(|(name, _)| format!("{}=>{}", escape(name), get_name(name)))
.join(",")
.map(|(name, _)| format!("{}=>&{},", escape(name), get_name(name)))
.join(""),
get_name(default)
)?;
Ok(())

View File

@@ -2,9 +2,7 @@
"identifier": "migrated",
"description": "permissions that were migrated from v1",
"local": true,
"windows": [
"main"
],
"windows": ["main"],
"permissions": [
"path:default",
"event:default",
@@ -24,9 +22,7 @@
"fs:allow-exists",
{
"identifier": "fs:scope",
"allow": [
"res/*"
]
"allow": ["res/*"]
},
"window:allow-create",
"window:allow-center",
@@ -61,13 +57,14 @@
"window:allow-set-ignore-cursor-events",
"window:allow-start-dragging",
"webview:allow-print",
"shell:allow-kill",
{
"identifier": "shell:allow-execute",
"allow": [
{
"args": true,
"cmd": "res/bin/aria2c",
"name": "ria2c",
"name": "aria2c",
"sidecar": false
},
{
@@ -142,7 +139,8 @@
"sidecar": false
}
]
}, {
},
{
"identifier": "shell:allow-open",
"allow": [
{
@@ -198,4 +196,4 @@
"process:default",
"os:default"
]
}
}

View File

@@ -1 +1 @@
{"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","fs:allow-read-file","fs:allow-write-file","fs:allow-read-dir","fs:allow-copy-file","fs:allow-mkdir","fs:allow-remove","fs:allow-remove","fs:allow-rename","fs:allow-exists",{"identifier":"fs:scope","allow":["res/*"]},"window:allow-create","window:allow-center","window:allow-request-user-attention","window:allow-set-resizable","window:allow-set-maximizable","window:allow-set-minimizable","window:allow-set-closable","window:allow-set-title","window:allow-maximize","window:allow-unmaximize","window:allow-minimize","window:allow-unminimize","window:allow-show","window:allow-hide","window:allow-close","window:allow-set-decorations","window:allow-set-always-on-top","window:allow-set-content-protected","window:allow-set-size","window:allow-set-min-size","window:allow-set-max-size","window:allow-set-position","window:allow-set-fullscreen","window:allow-set-focus","window:allow-set-icon","window:allow-set-skip-taskbar","window:allow-set-cursor-grab","window:allow-set-cursor-visible","window:allow-set-cursor-icon","window:allow-set-cursor-position","window:allow-set-ignore-cursor-events","window:allow-start-dragging","webview:allow-print",{"identifier":"shell:allow-execute","allow":[{"args":true,"cmd":"res/bin/aria2c","name":"ria2c","sidecar":false},{"args":true,"cmd":"res/bin/rclone","name":"rclone","sidecar":false},{"args":true,"cmd":"msiexec","name":"msiexec","sidecar":false},{"args":true,"cmd":"curl","name":"curl","sidecar":false},{"args":true,"cmd":"explorer","name":"explorer","sidecar":false},{"args":true,"cmd":"res/bin/alist/alist","name":"alist","sidecar":false}]},{"identifier":"shell:allow-spawn","allow":[{"args":true,"cmd":"res/bin/aria2c","name":"ria2c","sidecar":false},{"args":true,"cmd":"res/bin/rclone","name":"rclone","sidecar":false},{"args":true,"cmd":"msiexec","name":"msiexec","sidecar":false},{"args":true,"cmd":"curl","name":"curl","sidecar":false},{"args":true,"cmd":"explorer","name":"explorer","sidecar":false},{"args":true,"cmd":"res/bin/alist/alist","name":"alist","sidecar":false}]},{"identifier":"shell:allow-open","allow":[{"args":true,"cmd":"res/bin/aria2c","name":"ria2c","sidecar":false},{"args":true,"cmd":"res/bin/rclone","name":"rclone","sidecar":false},{"args":true,"cmd":"msiexec","name":"msiexec","sidecar":false},{"args":true,"cmd":"curl","name":"curl","sidecar":false},{"args":true,"cmd":"explorer","name":"explorer","sidecar":false},{"args":true,"cmd":"res/bin/alist/alist","name":"alist","sidecar":false}]},"os:allow-platform","os:allow-version","os:allow-os-type","os:allow-family","os:allow-arch","os:allow-exe-extension","os:allow-locale","os:allow-hostname","process:allow-restart","process:allow-exit","fs:default","shell:default","process:default","os:default"]}}
{"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","fs:allow-read-file","fs:allow-write-file","fs:allow-read-dir","fs:allow-copy-file","fs:allow-mkdir","fs:allow-remove","fs:allow-remove","fs:allow-rename","fs:allow-exists",{"identifier":"fs:scope","allow":["res/*"]},"window:allow-create","window:allow-center","window:allow-request-user-attention","window:allow-set-resizable","window:allow-set-maximizable","window:allow-set-minimizable","window:allow-set-closable","window:allow-set-title","window:allow-maximize","window:allow-unmaximize","window:allow-minimize","window:allow-unminimize","window:allow-show","window:allow-hide","window:allow-close","window:allow-set-decorations","window:allow-set-always-on-top","window:allow-set-content-protected","window:allow-set-size","window:allow-set-min-size","window:allow-set-max-size","window:allow-set-position","window:allow-set-fullscreen","window:allow-set-focus","window:allow-set-icon","window:allow-set-skip-taskbar","window:allow-set-cursor-grab","window:allow-set-cursor-visible","window:allow-set-cursor-icon","window:allow-set-cursor-position","window:allow-set-ignore-cursor-events","window:allow-start-dragging","webview:allow-print","shell:allow-kill",{"identifier":"shell:allow-execute","allow":[{"args":true,"cmd":"res/bin/aria2c","name":"aria2c","sidecar":false},{"args":true,"cmd":"res/bin/rclone","name":"rclone","sidecar":false},{"args":true,"cmd":"msiexec","name":"msiexec","sidecar":false},{"args":true,"cmd":"curl","name":"curl","sidecar":false},{"args":true,"cmd":"explorer","name":"explorer","sidecar":false},{"args":true,"cmd":"res/bin/alist/alist","name":"alist","sidecar":false}]},{"identifier":"shell:allow-spawn","allow":[{"args":true,"cmd":"res/bin/aria2c","name":"ria2c","sidecar":false},{"args":true,"cmd":"res/bin/rclone","name":"rclone","sidecar":false},{"args":true,"cmd":"msiexec","name":"msiexec","sidecar":false},{"args":true,"cmd":"curl","name":"curl","sidecar":false},{"args":true,"cmd":"explorer","name":"explorer","sidecar":false},{"args":true,"cmd":"res/bin/alist/alist","name":"alist","sidecar":false}]},{"identifier":"shell:allow-open","allow":[{"args":true,"cmd":"res/bin/aria2c","name":"ria2c","sidecar":false},{"args":true,"cmd":"res/bin/rclone","name":"rclone","sidecar":false},{"args":true,"cmd":"msiexec","name":"msiexec","sidecar":false},{"args":true,"cmd":"curl","name":"curl","sidecar":false},{"args":true,"cmd":"explorer","name":"explorer","sidecar":false},{"args":true,"cmd":"res/bin/alist/alist","name":"alist","sidecar":false}]},"os:allow-platform","os:allow-version","os:allow-os-type","os:allow-family","os:allow-arch","os:allow-exe-extension","os:allow-locale","os:allow-hostname","process:allow-restart","process:allow-exit","fs:default","shell:default","process:default","os:default"]}}

View File

@@ -1,34 +1,27 @@
use std::sync::RwLock;
use rand::distributions::DistString as _;
use crate::State;
fn random_str(len: usize) -> String {
rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), len)
}
pub struct ConfigState(pub RwLock<Config>);
#[derive(Clone)]
pub struct Config(pub serde_json::Value);
impl Config {
pub fn get(&self, key: String) -> Option<serde_json::Value> {
let parts = key.split(".");
Some(parts.fold(self.0.clone(), |value, part| value.get(part).unwrap().clone()))
}
}
impl State for Config {}
impl Default for Config {
fn default() -> Self {
Self(serde_json::json!({
"mount": { "lists": [] },
"task": [],
"api": { "url": "https://api.hotpe.top/API/NetMount" },
"settings": { "themeMode": "auto", "startHide": false },
"framework": {
"rclone": { "user": random_str(32), "password": random_str(128) },
"alist": { "user": "admin", "password": random_str(16) }
}
"mount": { "lists": [] },
"task": [],
"api": { "url": "https://api.hotpe.top/API/NetMount" },
"settings": { "themeMode": "auto", "startHide": false },
"framework": {
"rclone": { "user": random_str(32), "password": random_str(128) },
"alist": { "user": "admin", "password": random_str(16) }
}
}))
}
}

29
src-tauri/src/fs.rs Normal file
View File

@@ -0,0 +1,29 @@
use std::path::{Path, PathBuf};
use tauri::Manager as _;
use crate::Runtime;
fn resolve_path(app: &tauri::AppHandle<Runtime>, path: &str) -> PathBuf {
if path.starts_with("~") {
app.path().home_dir().unwrap().join(&path[1..]) // 跳过波浪线
} else {
Path::new(path).to_owned()
}
}
#[tauri::command]
pub fn fs_exist_dir(app: tauri::AppHandle<Runtime>, path: &str) -> anyhow_tauri::TAResult<bool> {
let path = resolve_path(&app, path);
let exists = std::fs::metadata(path)
.map_err(anyhow::Error::from)?
.is_dir();
Ok(exists)
}
#[tauri::command]
pub fn fs_make_dir(app: tauri::AppHandle<Runtime>, path: &str) -> anyhow_tauri::TAResult<()> {
let path = resolve_path(&app, path);
std::fs::create_dir_all(path).map_err(anyhow::Error::from)?;
Ok(())
}

View File

@@ -1,30 +1,18 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use config::Config;
use config::ConfigState;
use locale::Locale;
use locale::LocaleState;
use serde_json::{to_string_pretty, Value};
use std::fs::File;
use std::ops::Deref;
use std::sync::RwLock;
use tray::Tray;
use tray::TrayState;
//use tauri::AppHandle;
use std::env;
//use std::error::Error;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::{env, fs::File, ops::Deref, path::Path, sync::RwLock};
//use std::io::Read;
//use std::path::Path;
use tauri::Manager as _;
use config::Config;
use fs::{fs_exist_dir, fs_make_dir};
use locale::Locale;
use tray::Tray;
mod autostart;
mod config;
pub mod locale;
mod localized;
mod fs;
mod locale;
mod tray;
mod utils;
@@ -34,7 +22,6 @@ use crate::utils::download_with_progress;
// use crate::utils::ensure_single_instance;
#[cfg(target_os = "windows")]
use crate::utils::find_first_available_drive_letter;
use crate::utils::get_home_dir;
#[cfg(target_os = "windows")]
use crate::utils::is_winfsp_installed;
#[cfg(target_os = "windows")]
@@ -42,59 +29,61 @@ use crate::utils::set_window_shadow;
pub(crate) type Runtime = tauri::Wry;
pub trait State: Send + Sync + 'static {}
pub struct StateWrapper<T: State>(RwLock<T>);
pub trait AppExt {
fn main_window(&self) -> tauri::WebviewWindow<Runtime>;
fn app_locale(&self) -> &Locale;
fn app_config(&self) -> &Config;
fn app_main_window(&self) -> tauri::WebviewWindow<Runtime>;
fn with_app_state<T: State, R>(&self, closure: impl FnOnce(&T) -> R) -> R;
fn set_app_state<T: State>(&self, state: T);
fn update_app_config(&self) -> anyhow::Result<()>;
fn write_app_config(&self, config: Config) -> anyhow::Result<()>;
fn app_data_dir(&self) -> PathBuf;
fn app_config_file(&self) -> PathBuf;
fn quit(&self);
fn app_quit(&self);
fn app_restart(&self);
}
impl<M: tauri::Manager<Runtime>> AppExt for M {
fn main_window(&self) -> tauri::WebviewWindow {
fn app_main_window(&self) -> tauri::WebviewWindow {
self.get_webview_window("main").unwrap()
}
fn app_locale(&self) -> Locale {
self.state::<LocaleState>()
.deref()
.0
.read()
.unwrap()
.as_ref()
.unwrap()
fn with_app_state<T: State, R>(&self, closure: impl FnOnce(&T) -> R) -> R {
let wrapper = self.state::<StateWrapper<T>>();
let state = wrapper.deref().0.read().unwrap();
closure(state.deref())
}
fn app_config(&self) -> &Config {
self.state::<ConfigState>()
.deref()
.0
.read()
.unwrap()
.deref()
fn set_app_state<T: State>(&self, state: T) {
if let Some(wrapper) = self.try_state::<StateWrapper<T>>() {
*wrapper.deref().0.write().unwrap() = state;
} else {
self.manage(StateWrapper(RwLock::new(state)));
}
}
fn update_app_config(&self) -> anyhow::Result<()> {
let config = self.app_config();
let current_locale = tauri_plugin_os::locale().unwrap_or_else(|| "C".into());
*self.state::<LocaleState>().deref().0.write().unwrap() = Some(Locale::new(
config.0["settings"]
.get("language")
.map(|item| item.as_str().unwrap())
.unwrap_or_else(|| &current_locale),
));
*self.state::<TrayState>().deref().0.write().unwrap() = Some(Tray::new(self.app_handle())?);
Ok(())
self.with_app_state::<Config, _>(|config| {
let current_locale = tauri_plugin_os::locale().unwrap_or_else(|| "C".into());
self.set_app_state(Locale::new(
config.0["settings"]
.get("language")
.map(|item| item.as_str().unwrap())
.unwrap_or_else(|| &current_locale),
));
self.set_app_state(Tray::new(self.app_handle())?);
Ok(())
})
}
fn write_app_config(&self, config: Config) -> anyhow::Result<()> {
*self.state::<ConfigState>().deref().0.write().unwrap() = config;
let mut file = File::create(self.app_config_file())?;
serde_json::to_writer_pretty(file, &self.app_config().0)?;
self.set_app_state(config);
let file = File::create(self.app_config_file())?;
serde_json::to_writer_pretty(
file,
&self.with_app_state::<Config, _>(|config| config.0.clone()),
)?;
Ok(())
}
@@ -106,9 +95,13 @@ impl<M: tauri::Manager<Runtime>> AppExt for M {
self.app_data_dir().join("config.json")
}
fn quit(&self) {
fn app_quit(&self) {
self.app_handle().exit(0)
}
fn app_restart(&self) {
self.app_handle().restart()
}
}
pub trait WindowExt {
@@ -116,7 +109,7 @@ pub trait WindowExt {
fn toggle_visibility(&self, preferred_show: Option<bool>) -> anyhow::Result<()>;
}
impl WindowExt for tauri::WebviewWindow {
impl WindowExt for tauri::WebviewWindow<Runtime> {
fn toggle_devtools(&self, preferred_open: Option<bool>) {
let open = preferred_open.unwrap_or_else(|| !self.is_devtools_open());
if open {
@@ -138,17 +131,8 @@ impl WindowExt for tauri::WebviewWindow {
}
}
const USER_DATA_PATH: &str = ".netmount";
const CONFIG_FILE: &str = "config.json";
pub fn init() -> anyhow::Result<()> {
let home_dir = get_home_dir();
if home_dir.join(USER_DATA_PATH).exists() {
fs::create_dir_all(home_dir.join(USER_DATA_PATH)).unwrap()
}
//设置运行目录
// 设置运行目录
let exe_dir = env::current_exe()
.expect("无法获取当前可执行文件路径")
.parent()
@@ -188,48 +172,38 @@ pub fn init() -> anyhow::Result<()> {
}
}
//run_command("ls").expect("运行ls命令失败");
//run_command("dir").expect("运行ls命令失败");
// 根据不同的操作系统配置Tauri Builder
let builder = tauri::Builder::default()
tauri::Builder::default()
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_single_instance::init(|app, _, _| {
app.main_window().toggle_visibility(Some(true));
app.app_main_window().toggle_visibility(Some(true)).ok();
}))
.invoke_handler(tauri::generate_handler![
// TODO: alternatives?
// set_localized,
read_config_file,
write_config_file,
toggle_devtools,
get_config,
update_config,
get_language_pack,
download_file,
get_autostart_state,
set_autostart_state,
get_winfsp_install_state,
get_available_drive_letter,
// TODO: alternatives?
// set_devtools_state,
get_available_ports,
fs_exist_dir,
fs_make_dir,
restart_self,
get_available_ports
restart_self
])
.setup(|app| {
app.manage(ConfigState(RwLock::new(
if let Some(file) = File::open(app.app_config_file()).ok() {
Config(serde_json::from_reader(file)?)
} else {
Config::default()
},
)));
app.manage(LocaleState(RwLock::new(None)));
app.manage(TrayState(RwLock::new(None)));
if let Some(file) = File::open(app.app_config_file()).ok() {
app.set_app_state(Config(serde_json::from_reader(file)?))
} else {
app.write_app_config(Config::default())?
};
app.update_app_config()?;
#[cfg(debug_assertions)]
app.main_window().toggle_devtools(Some(true));
app.app_main_window().toggle_devtools(Some(true));
Ok(())
})
.run(tauri::generate_context!())?;
@@ -237,92 +211,40 @@ pub fn init() -> anyhow::Result<()> {
}
#[tauri::command]
fn toggle_devtools(window: tauri::WebviewWindow, preferred_open: Option<bool>) {
fn toggle_devtools(window: tauri::WebviewWindow<Runtime>, preferred_open: Option<bool>) {
window.toggle_devtools(preferred_open)
}
use std::io::ErrorKind;
use std::path::PathBuf;
#[tauri::command]
fn fs_exist_dir(path: &str) -> bool {
let home_dir = get_home_dir();
// 替换路径中的波浪线 (~) 为用home目录
let mut resolved_path = PathBuf::new();
if path.starts_with("~") {
resolved_path.push(home_dir);
resolved_path.push(&path[1..]); // 跳过波浪线
} else {
resolved_path.push(path);
}
is_directory(path)
fn get_language_pack(app: tauri::AppHandle<Runtime>) -> serde_json::Value {
serde_json::Value::Object(serde_json::value::Map::from_iter(
app.with_app_state::<Locale, _>(|locale| locale.0)
.entries()
.map(|(&key, &value)| (key.into(), serde_json::Value::String(value.into()))),
))
.into()
}
#[tauri::command]
fn fs_make_dir(path: &str) -> bool {
let home_dir = get_home_dir();
// 替换路径中的波浪线 (~) 为用home目录
let mut resolved_path = PathBuf::new();
if path.starts_with("~") {
resolved_path.push(home_dir);
resolved_path.push(&path[1..]); // 跳过波浪线
} else {
resolved_path.push(path);
}
// 创建所有必要的父目录
if let Some(parent) = resolved_path.parent() {
if let Err(e) = fs::create_dir_all(parent) {
match e.kind() {
ErrorKind::NotFound => (),
_ => {
eprintln!("Error preparing directory structure: {}", e);
return false;
}
}
}
}
// 尝试创建目标目录
match fs::create_dir(&resolved_path) {
Ok(_) => true,
Err(e) => {
eprintln!("Error creating directory: {}", e);
false
}
}
}
fn is_directory(path: &str) -> bool {
match fs::metadata(path) {
Ok(metadata) => metadata.is_dir(),
Err(_) => false,
}
fn get_config(app: tauri::AppHandle<Runtime>) -> serde_json::Value {
app.with_app_state::<Config, _>(|config| config.0.clone())
}
#[tauri::command]
fn restart_self() {
utils::restart_self()
}
use std::error::Error;
use std::process::Command;
fn run_command(cmd: &str) -> Result<(), Box<dyn Error>> {
let cmd_str = if cfg!(target_os = "windows") {
format!("{}", cmd.replace("/", "\\"))
} else {
format!("{}", cmd)
};
let child = if cfg!(target_os = "windows") {
Command::new("cmd").arg("/c").arg(cmd_str).spawn()?
} else {
Command::new("sh").arg("-c").arg(cmd_str).spawn()?
};
child.wait_with_output()?;
fn update_config(
app: tauri::AppHandle<Runtime>,
data: serde_json::Value,
) -> anyhow_tauri::TAResult<()> {
app.write_app_config(Config(data))?;
app.update_app_config()?;
Ok(())
}
#[tauri::command]
fn restart_self(app: tauri::AppHandle<Runtime>) {
app.restart()
}
#[tauri::command]
fn get_winfsp_install_state() -> Result<bool, usize> {
#[cfg(not(target_os = "windows"))]
@@ -375,56 +297,6 @@ fn get_available_drive_letter() -> Result<String, String> {
}
}
#[tauri::command]
fn get_config(app: tauri::AppHandle) -> Config {
app.app_config().clone()
}
#[tauri::command]
fn update_config(app: tauri::AppHandle, data: Value) -> anyhow_tauri::TAResult<()> {
app.write_app_config(Config(data));
app.update_app_config()?;
Ok(())
}
#[tauri::command]
fn read_config_file(path: Option<&str>) -> Result<Value, String> {
let path = path.unwrap_or(CONFIG_FILE);
let home_dir = get_home_dir();
let content_result = fs::read_to_string(if path == CONFIG_FILE {
home_dir.join(USER_DATA_PATH).join(path)
} else {
PathBuf::from(path)
});
match content_result {
Ok(content) => match serde_json::from_str(&content) {
Ok(config) => Ok(config),
Err(json_error) => Err(format!("Failed to parse JSON from file: {}", json_error)),
},
Err(io_error) => Err(format!("Failed to read file: {}", io_error)),
}
}
#[tauri::command]
async fn write_config_file(config_data: Value, path: Option<&str>) -> Result<(), String> {
let path = path.unwrap_or(CONFIG_FILE);
let home_dir = get_home_dir();
let pretty_config = to_string_pretty(&config_data)
.map_err(|json_error| format!("Failed to serialize JSON: {}", json_error))?;
fs::write(
if path == CONFIG_FILE {
home_dir.join(USER_DATA_PATH).join(path)
} else {
PathBuf::from(path)
},
pretty_config,
)
.map_err(|io_error| format!("Failed to write file: {}", io_error))?;
Ok(())
}
#[tauri::command]
fn get_available_ports(count: usize) -> Vec<u16> {
return utils::get_available_ports(count);

View File

@@ -1,10 +1,8 @@
use std::sync::RwLock;
use crate::State;
include!(concat!(env!("OUT_DIR"), "/codegen.rs"));
include!(concat!(env!("OUT_DIR"), "/language.rs"));
pub struct LocaleState(pub RwLock<Option<Locale>>);
pub struct Locale(pub Pack);
pub struct Locale(pub &'static Pack);
impl Locale {
pub fn new(name: &str) -> Self {
@@ -15,3 +13,5 @@ impl Locale {
self.0.get(id).unwrap()
}
}
impl State for Locale {}

View File

@@ -1,30 +0,0 @@
// use lazy_static::lazy_static;
// use serde_json::{Map, Value};
// use std::sync::{Mutex, RwLock};
// use tauri::Manager;
// use crate::tray::tray;
// lazy_static! {
// pub static ref LOCALIZATION: RwLock<Map<String, Value>> = RwLock::new(Map::new());
// }
// pub fn get_localized_text(key: &str) -> String {
// let lock = LANGUAGE_PACK.lock().unwrap();
// match lock.get(key) {
// Some(value) => value.as_str().unwrap_or(key).to_owned(),
// None => key.to_owned(),
// }
// }
// #[tauri::command]
// pub fn set_localized(app: tauri::AppHandle, localized_data: Value) -> Result<(), String> {
// let map=localized_data.as_object().map_err(||"Provided localized data is not a JSON object.")?;
// let language_pack_map: Map<String, Value> = map.clone().into();
// let mut pack = ;
// *LANGUAGE_PACK.lock().unwrap() = language_pack_map;
// app.manage(tray(app))
// }
// Ok(())
// }

View File

@@ -1,5 +1,5 @@
use app::init;
fn main(){
init()
fn main() {
init().unwrap();
}

View File

@@ -1,39 +1,56 @@
use std::sync::RwLock;
use crate::{AppExt, Locale, Runtime, State, WindowExt};
use crate::{AppExt, Runtime};
pub struct TrayState(pub RwLock<Option<Tray>>);
pub struct Tray(tauri::tray::TrayIcon<Runtime>);
pub struct Tray(pub tauri::tray::TrayIcon<Runtime>);
impl Tray {
pub fn new(app: &tauri::AppHandle<Runtime>) -> anyhow::Result<Self> {
let build_item = |id: &str, text: &str| {
tauri::menu::MenuItemBuilder::with_id(id, text)
.build(app)
.unwrap()
};
let menu = tauri::menu::MenuBuilder::new(app)
.items(&[
&build_item("show", app.app_locale().get("show&hide")),
&build_item("quit", app.app_locale().get("quit")),
])
.build()
.unwrap();
let tray = tauri::tray::TrayIconBuilder::new()
.menu(&menu)
.on_tray_icon_event(|app, event| {
// window.show().unwrap();
// window.set_focus().unwrap()
})
.on_menu_event(|app, event| {
match event.id.as_ref() {
"quit" => app.exit(0),
// "hide&show" => hide_or_show(),
app.with_app_state::<Locale, _>(|locale| {
let build_item = |id: &str, text: &str| {
tauri::menu::MenuItemBuilder::with_id(id, text)
.build(app)
.unwrap()
};
let menu = tauri::menu::MenuBuilder::new(app)
.items(&[
&build_item("show", locale.get("tray_show")),
&build_item("quit", locale.get("quit")),
])
.build()
.unwrap();
let tray = tauri::tray::TrayIconBuilder::new()
.menu(&menu)
.on_tray_icon_event(|icon, event| match event {
tauri::tray::TrayIconEvent::Click {
id: _,
position: _,
rect: _,
button,
button_state,
} => {
if button == tauri::tray::MouseButton::Left
&& button_state == tauri::tray::MouseButtonState::Up
{
icon.app_handle()
.app_main_window()
.toggle_visibility(None)
.ok();
}
}
_ => {}
}
})
.build(app)?;
Ok(Self(tray))
})
.on_menu_event(|app, event| match event.id.as_ref() {
"show" => {
app.app_main_window().toggle_visibility(Some(true)).ok();
}
"quit" => {
app.app_quit();
}
_ => {}
})
.build(app)?;
Ok(Self(tray))
})
}
}
impl State for Tray {}

View File

View File

@@ -1,16 +1,11 @@
#[cfg(target_os = "windows")]
use tauri::{Manager, Runtime};
#[cfg(target_os = "windows")]
use window_shadows::set_shadow;
#[cfg(target_os = "windows")]
use std::error::Error;
#[cfg(target_os = "windows")]
use std::fs;
use std::io::{self, Write};
//use tauri::AppHandle;
pub fn get_available_ports(count: usize) -> Vec<u16> {
use std::net::TcpListener;
@@ -23,28 +18,6 @@ pub fn get_available_ports(count: usize) -> Vec<u16> {
ports
}
#[cfg(target_os = "windows")]
pub fn set_window_shadow<R: Runtime>(app: &tauri::App<R>) {
{
use raw_window_handle::HasRawWindowHandle;
fn set_shadow<W: HasRawWindowHandle>(_window: &W, enabled: bool) -> Result<(), Box<dyn Error>> {
set_shadow(_window, true).expect("Unsupported platform!");
Ok(())
}
let window_map = app.get_webview_window("main").unwrap().webview_windows();
// 假设webview_windows返回了一个包含窗口名String和窗口实例WebviewWindow的映射
// 我们需要获取映射中的值即WebviewWindow实例
if let Some(webview_window) = window_map.values().next() {
} else {
eprintln!("No webview window found.");
}
}
}
#[cfg(target_os = "windows")]
#[tauri::command]
pub fn find_first_available_drive_letter() -> Result<Option<String>, io::Error> {
@@ -141,123 +114,3 @@ pub fn is_winfsp_installed() -> Result<bool, Box<dyn Error>> {
Ok(false)
}
}
pub fn get_home_dir() -> std::path::PathBuf {
use directories::UserDirs;
let user_dirs = UserDirs::new().expect("Failed to get user dirs");
user_dirs.home_dir().to_path_buf()
}
pub fn restart_self() {
// 重启自身
use std::ffi::OsString;
use std::process::Command;
let args: Vec<String> = std::env::args().skip(1).collect();
let os_args: Vec<OsString> = args.into_iter().map(OsString::from).collect();
Command::new(std::env::args().next().unwrap())
.args(os_args)
.spawn()
.expect("Failed to restart the process");
std::process::exit(0);
}
// pub fn ensure_single_instance(user_data_path: &str) {
// fn message_dialog() {
// use rfd::MessageDialog;
// MessageDialog::new()
// .set_title("Warning")
// .set_description("Process already exists!")
// .show();
// }
// let home_dir = get_home_dir();
// let _ = &home_dir.join(user_data_path);
// /* //文件锁
// use fslock::LockFile;
// let pid_path = home_dir.join(user_data_path).join("NetMount.lock");
// // 打开pid文件没有则自动创建
// let mut pid_lock = LockFile::open(&pid_path.clone().into_os_string()).unwrap();
// // 非阻塞的锁文件
// if !pid_lock.try_lock_with_pid().unwrap() {
// message_dialog();
// panic!("An instance of this application is already running, exiting now.");
// // 如果文件已经被锁,则退出进程
// } */
// // 进程名
// use std::sync::Mutex;
// use once_cell::sync::Lazy;
// use std::collections::HashSet;
// use sysinfo::{Pid, System};
// let current_pid = sysinfo::get_current_pid().expect("Failed to get current PID");
// let current_proc_name = std::env::args().next().unwrap_or_default();
// let mut system = System::new_all();
// system.refresh_all();
// static EXISTING_PIDS: Lazy<Mutex<HashSet<Pid>>> = Lazy::new(|| Mutex::new(HashSet::new()));
// {
// let mut existing_pids = EXISTING_PIDS.lock().expect("Failed to lock PID set");
// for (pid, proc_) in system.processes() {
// if proc_.name() == current_proc_name && *pid != current_pid {
// existing_pids.insert(*pid);
// }
// }
// if !existing_pids.is_empty() {
// message_dialog();
// panic!(
// "An instance of this application is already running (PIDs: {:?}), exiting now.",
// *existing_pids
// );
// }
// }
// #[cfg(target_os = "windows")]
// {
// extern crate winapi;
// extern crate widestring;
// use widestring::U16CString;
// //use winapi::shared::ntdef::NULL;
// use winapi::shared::winerror::ERROR_ALREADY_EXISTS;
// use winapi::um::errhandlingapi::GetLastError;
// use winapi::um::synchapi::CreateMutexW;
// // 定义互斥体名称
// let mutex_name = U16CString::from_str("NetMount").expect("Failed to create U16CString");
// // 创建互斥体
// unsafe {
// let handle = CreateMutexW(
// std::ptr::null_mut(),
// winapi::shared::minwindef::FALSE,
// mutex_name.as_ptr(),
// );
// // 检查互斥体是否已经创建
// if !handle.is_null() && GetLastError() == ERROR_ALREADY_EXISTS {
// // 有效句柄,但是互斥体已存在
// message_dialog();
// panic!("Another instance of the application is already running.");
// } else if !handle.is_null() {
// // 互斥体创建成功,且无先前存在的实例
// println!("Application instance is running.");
// // 在这里执行应用程序逻辑
// // ...
// // 在程序结束前,应该关闭互斥体句柄(此行代码并未在示例中展示)
// } else {
// // 创建互斥体失败,可能要进行错误处理
// //panic!("Failed to create mutex.");
// }
// }
// }
// }

View File

@@ -1,33 +1,35 @@
//本地化
import { invoke } from "@tauri-apps/api/core";
import { t } from "i18next";
import i18n from "../../services/i18n";
import { roConfig } from "../../services/config";
import { hooks } from "../../services/hook";
async function setLocalized(lang: string) {
lang = lang.toLowerCase()
hooks.setLocaleStr(getLangCode(lang))
i18n.changeLanguage(lang)
await invoke('set_localized', {
localizedData: {
quit: t("quit"),
show: t("tray_show"),
hide: t("tray_hide")
}
})
lang = lang.toLowerCase();
hooks.setLocaleStr(getLangCode(lang));
const pack: Record<string, string> = await invoke("get_language_pack");
i18n.addResourceBundle(lang, "translation", pack);
i18n.changeLanguage(lang);
// TODO: remove comment
// await invoke('set_localized', {
// localizedData: {
// quit: t("quit"),
// show: t("tray_show"),
// hide: t("tray_hide")
// }
// })
}
function getLangCode(lang: string): string {
for (const value of roConfig.options.setting.language.select) {
if (lang === value.value) {
return value.langCode
}
for (const value of roConfig.options.setting.language.select) {
if (lang === value.value) {
return value.langCode;
}
return roConfig.options.setting.language.select[roConfig.options.setting.language.defIndex].langCode
}
return roConfig.options.setting.language.select[
roConfig.options.setting.language.defIndex
].langCode;
}
export { setLocalized, getLangCode }
export { setLocalized, getLangCode };

View File

@@ -1,123 +1,140 @@
import { invoke } from "@tauri-apps/api/core"
import { nmConfig, saveNmConfig } from "../../../services/config"
import { hooks } from "../../../services/hook"
import { rcloneInfo } from "../../../services/rclone"
import { MountListItem } from "../../../type/config"
import { ParametersType } from "../../../type/defaults"
import { rclone_api_post } from "../../../utils/rclone/request"
import { fs_exist_dir, fs_make_dir } from "../../../utils/utils"
import { convertStoragePath, formatPathRclone } from "../storage"
import { MountOptions, VfsOptions } from "../../../type/rclone/storage/mount/parameters"
import { invoke } from "@tauri-apps/api/core";
import { nmConfig, saveNmConfig } from "../../../services/config";
import { hooks } from "../../../services/hook";
import { rcloneInfo } from "../../../services/rclone";
import { MountListItem } from "../../../type/config";
import { ParametersType } from "../../../type/defaults";
import { rclone_api_post } from "../../../utils/rclone/request";
import { fs_exist_dir, fs_make_dir } from "../../../utils/utils";
import { convertStoragePath, formatPathRclone } from "../storage";
import {
MountOptions,
VfsOptions,
} from "../../../type/rclone/storage/mount/parameters";
//列举存储
async function reupMount(noRefreshUI?: boolean) {
const mountPoints: any[] =
(await rclone_api_post("/mount/listmounts"))?.mountPoints || [];
const mountPoints: any[] = (await rclone_api_post(
'/mount/listmounts',
)).mountPoints || []
rcloneInfo.mountList = [];
rcloneInfo.mountList = [];
mountPoints.forEach((tiem: any) => {
const name = tiem.Fs
rcloneInfo.mountList.push({
storageName: name, //name.substring(0, name.length - 1)
mountPath: tiem.MountPoint,
mountedTime: new Date(tiem.MountedOn),
})
mountPoints.forEach((tiem: any) => {
const name = tiem.Fs;
rcloneInfo.mountList.push({
storageName: name, //name.substring(0, name.length - 1)
mountPath: tiem.MountPoint,
mountedTime: new Date(tiem.MountedOn),
});
!noRefreshUI && hooks.upMount()
});
!noRefreshUI && hooks.upMount();
}
function getMountStorage(mountPath: string): MountListItem | undefined {
return nmConfig.mount.lists.find((item) => item.mountPath === mountPath)
return nmConfig.mount.lists.find((item) => item.mountPath === mountPath);
}
function isMounted(mountPath: string): boolean {
return rcloneInfo.mountList.findIndex((item) => item.mountPath === mountPath) !== -1
return (
rcloneInfo.mountList.findIndex((item) => item.mountPath === mountPath) !==
-1
);
}
async function addMountStorage(storageName: string, mountPath: string, parameters: { vfsOpt: VfsOptions, mountOpt: MountOptions }, autoMount?: boolean) {
if (getMountStorage(mountPath)) {
return false
}
async function addMountStorage(
storageName: string,
mountPath: string,
parameters: { vfsOpt: VfsOptions; mountOpt: MountOptions },
autoMount?: boolean,
) {
if (getMountStorage(mountPath)) {
return false;
}
const mountInfo: MountListItem = {
storageName: storageName,
mountPath: mountPath,
parameters: parameters,
autoMount: (autoMount || false),
}
nmConfig.mount.lists.push(mountInfo)
const mountInfo: MountListItem = {
storageName: storageName,
mountPath: mountPath,
parameters: parameters,
autoMount: autoMount || false,
};
nmConfig.mount.lists.push(mountInfo);
await saveNmConfig()
await reupMount()
await saveNmConfig();
await reupMount();
}
async function delMountStorage(mountPath: string) {
if (isMounted(mountPath)) {
await unmountStorage(mountPath)
if (isMounted(mountPath)) {
await unmountStorage(mountPath);
}
nmConfig.mount.lists.forEach((item, index) => {
if (item.mountPath === mountPath) {
nmConfig.mount.lists.splice(index, 1);
}
});
nmConfig.mount.lists.forEach((item, index) => {
if (item.mountPath === mountPath) {
nmConfig.mount.lists.splice(index, 1)
}
})
await saveNmConfig()
await reupMount()
await saveNmConfig();
await reupMount();
}
async function editMountStorage(mountInfo: MountListItem) {
//await reupMount()
//觉得这里是不必要的,就注释了
/*rcloneInfo.mountList.forEach((item) => {
//await reupMount()
//觉得这里是不必要的,就注释了
/*rcloneInfo.mountList.forEach((item) => {
if (item.mountPath === mountInfo.mountPath) {
return false
}
}) */
for (let i = 0; i < nmConfig.mount.lists.length; i++) {
if (nmConfig.mount.lists[i].mountPath === mountInfo.mountPath) {
nmConfig.mount.lists[i] = mountInfo
break
}
for (let i = 0; i < nmConfig.mount.lists.length; i++) {
if (nmConfig.mount.lists[i].mountPath === mountInfo.mountPath) {
nmConfig.mount.lists[i] = mountInfo;
break;
}
}
await saveNmConfig()
await saveNmConfig();
}
async function mountStorage(mountInfo: MountListItem) {
if (!rcloneInfo.version.os.toLowerCase().includes('windows') && !await fs_exist_dir(mountInfo.mountPath)) {
await fs_make_dir(mountInfo.mountPath)
}
if (
!rcloneInfo.version.os.toLowerCase().includes("windows") &&
!(await fs_exist_dir(mountInfo.mountPath))
) {
await fs_make_dir(mountInfo.mountPath);
}
const back = await rclone_api_post('/mount/mount', {
fs: convertStoragePath(mountInfo.storageName) || mountInfo.storageName,
mountPoint: mountInfo.mountPath,
...(mountInfo.parameters)
})
const back = await rclone_api_post("/mount/mount", {
fs: convertStoragePath(mountInfo.storageName) || mountInfo.storageName,
mountPoint: mountInfo.mountPath,
...mountInfo.parameters,
});
await reupMount()
return back
await reupMount();
return back;
}
async function unmountStorage(mountPath: string) {
await rclone_api_post('/mount/unmount', {
mountPoint: mountPath,
})
await rclone_api_post("/mount/unmount", {
mountPoint: mountPath,
});
await reupMount()
await reupMount();
}
async function getAvailableDriveLetter(): Promise<string> {
return await invoke('get_available_drive_letter')//Z:
return await invoke("get_available_drive_letter"); //Z:
}
export { reupMount, mountStorage, unmountStorage, addMountStorage, delMountStorage, editMountStorage, getMountStorage, isMounted, getAvailableDriveLetter }
export {
reupMount,
mountStorage,
unmountStorage,
addMountStorage,
delMountStorage,
editMountStorage,
getMountStorage,
isMounted,
getAvailableDriveLetter,
};

View File

@@ -124,10 +124,10 @@ function Mount_page() {
const mounted = isMounted(item.mountPath)
return {
...item,
mountPath_: <div style={{ display: 'flex', alignItems:'center' }}><Typography.Ellipsis className='singe-line' showTooltip>{item.mountPath}</Typography.Ellipsis>{rcloneInfo.endpoint.isLocal&&osInfo.osType==='windows' &&mounted&&
<Button title={t('show_path_in_explorer')} onClick={async () => {
await showPathInExplorer(item.mountPath,true)
}} type='text' icon={<IconEye />}></Button>}</div>,
mountPath_: <div style={{ display: 'flex', alignItems: 'center' }}><Typography.Ellipsis className='singe-line' showTooltip>{item.mountPath}</Typography.Ellipsis>{rcloneInfo.endpoint.isLocal && osInfo.osType === 'windows' && mounted &&
<Button title={t('show_path_in_explorer')} onClick={async () => {
await showPathInExplorer(item.mountPath, true)
}} type='text' icon={<IconEye />}></Button>}</div>,
mounted: mounted ? t('mounted') : t('unmounted'),
actions: <Space>
{

View File

@@ -83,15 +83,15 @@ const setNmConfig = (config: NMConfig) => {
}
const readNmConfig = async () => {
await invoke('read_config_file').then(configData => {
await invoke('get_config').then(configData => {
setNmConfig({ ...nmConfig, ...(configData as NMConfig) })
}).catch(err => {
console.log(err);
})
}
const saveNmConfig = async () => {
await invoke('write_config_file', {
configData: nmConfig
await invoke('update_config', {
data: nmConfig
});
}

View File

@@ -1,36 +1,12 @@
// i18n.js 文件
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
// 引入语言文件
import cn from '../controller/language/pack/zh-cn.json';
import en from '../controller/language/pack/en.json';
import ct from '../controller/language/pack/zh-hant.json';
// 初始化资源文件即各种语言的json文件
const resources = {
cn: {
translation: cn
i18n.use(initReactI18next).init({
resources: {},
keySeparator: false,
interpolation: {
escapeValue: false,
},
ct: {
translation: ct
},
en: {
translation: en
}
};
i18n
// 连接react-i18next与i18next的插件配置
.use(initReactI18next)
.init({
resources,
/*lng: "cn", // 初始语言 */
keySeparator: false, // 是否允许keys使用点分隔符
interpolation: {
escapeValue: false // 转义字符
}
});
});
export default i18n;

View File

@@ -20,10 +20,11 @@ async function setAlistPass(pass:string){
}
async function modifyAlistConfig(rewriteData:any=alistInfo.alistConfig){
const path = alistDataDir()+'config.json'
const oldAlistConfig =await invoke('read_config_file',{path}) as object
const newAlistConfig = {...oldAlistConfig, ...rewriteData}
await invoke('write_config_file',{configData:newAlistConfig,path:path})
// TODO: remove this
// const path = alistDataDir()+'config.json'
// const oldAlistConfig =await invoke('read_config_file',{path}) as object
// const newAlistConfig = {...oldAlistConfig, ...rewriteData}
// await invoke('write_config_file',{configData:newAlistConfig,path:path})
}
async function addAlistInRclone(){

View File

@@ -9,7 +9,7 @@ import { getAlistToken, modifyAlistConfig, setAlistPass } from "./alist";
import { alist_api_ping } from "./request";
const alistDataDir = () => {
return formatPath(roConfig.env.path.homeDir + '/.netmount/alist/',osInfo.osType==="windows")
return formatPath(roConfig.env.path.homeDir + '/.netmount/alist/', osInfo.osType === "windows")
}
const addParams = (): string[] => {

View File

@@ -142,8 +142,8 @@ export function compareVersions(v1: string, v2: string) {
}
export async function set_devtools_state(state: boolean) {
await invoke('set_devtools_state', {
state: state
await invoke('toggle_devtools', {
preferred_open: state
})
}
@@ -154,9 +154,14 @@ export async function fs_exist_dir(path: string) {
}
export async function fs_make_dir(path: string) {
return await invoke('fs_make_dir', {
path: path
}) as boolean
try {
await invoke('fs_make_dir', {
path: path
})
return true;
} catch {
return false;
}
}
export function formatPath(path: string, isWindows: boolean = false) {