mirror of
https://github.com/bolucat/Archive.git
synced 2025-09-26 20:21:35 +08:00
Update On Sun Feb 2 19:31:29 CET 2025
This commit is contained in:
1
.github/update.log
vendored
1
.github/update.log
vendored
@@ -901,3 +901,4 @@ Update On Wed Jan 29 19:35:42 CET 2025
|
||||
Update On Thu Jan 30 19:32:29 CET 2025
|
||||
Update On Fri Jan 31 19:32:11 CET 2025
|
||||
Update On Sat Feb 1 19:32:16 CET 2025
|
||||
Update On Sun Feb 2 19:31:20 CET 2025
|
||||
|
@@ -46,6 +46,12 @@ export default {
|
||||
],
|
||||
},
|
||||
],
|
||||
'at-rule-no-deprecated': [
|
||||
true,
|
||||
{
|
||||
ignoreAtRules: ['apply'],
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
1261
clash-nyanpasu/backend/Cargo.lock
generated
1261
clash-nyanpasu/backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -16,12 +16,17 @@ name = "nyanpasu-network-statistic-widget-small"
|
||||
path = "./src/small.rs"
|
||||
|
||||
[dependencies]
|
||||
eframe = "0.29.1"
|
||||
egui_extras = { version = "0.29", features = ["all_loaders"] }
|
||||
eframe = "0.30"
|
||||
egui_extras = { version = "0.30", features = ["all_loaders"] }
|
||||
parking_lot = "0.12"
|
||||
image = { version = "0.25.5", features = ["jpeg", "png"] }
|
||||
humansize = "2.1.3"
|
||||
image = { version = "0.25", features = ["jpeg", "png"] }
|
||||
humansize = "2"
|
||||
# for svg currentColor replacement
|
||||
resvg = "0.44.0" # for svg rendering
|
||||
usvg = "0.44.0" # for svg parsing
|
||||
csscolorparser = "0.7.0" # for color conversion
|
||||
resvg = "0.44" # for svg rendering
|
||||
usvg = "0.44" # for svg parsing
|
||||
csscolorparser = "0.7" # for color conversion
|
||||
ipc-channel = "0.19" # for IPC between the Widget process and the GUI process
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
anyhow = "1"
|
||||
specta = { version = "=2.0.0-rc.22", features = ["serde"] }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
67
clash-nyanpasu/backend/nyanpasu-egui/src/ipc.rs
Normal file
67
clash-nyanpasu/backend/nyanpasu-egui/src/ipc.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
pub use ipc_channel::ipc::IpcSender;
|
||||
use ipc_channel::ipc::{self, IpcReceiver};
|
||||
|
||||
use crate::widget::network_statistic_large::LogoPreset;
|
||||
|
||||
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||
pub struct StatisticMessage {
|
||||
pub download_total: u64,
|
||||
pub upload_total: u64,
|
||||
pub download_speed: u64,
|
||||
pub upload_speed: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub enum Message {
|
||||
Stop,
|
||||
UpdateStatistic(StatisticMessage),
|
||||
UpdateLogo(LogoPreset),
|
||||
}
|
||||
|
||||
pub struct IPCServer {
|
||||
oneshot_server: Option<ipc::IpcOneShotServer<IpcSender<Message>>>,
|
||||
tx: Option<IpcSender<Message>>,
|
||||
}
|
||||
|
||||
impl IPCServer {
|
||||
pub fn is_connected(&self) -> bool {
|
||||
self.tx.is_some()
|
||||
}
|
||||
|
||||
pub fn connect(&mut self) -> anyhow::Result<()> {
|
||||
if self.oneshot_server.is_none() {
|
||||
anyhow::bail!("IPC server is already initialized");
|
||||
}
|
||||
|
||||
let (_, tx) = self.oneshot_server.take().unwrap().accept()?;
|
||||
self.tx = Some(tx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn into_tx(self) -> Option<IpcSender<Message>> {
|
||||
self.tx
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_ipc_server() -> anyhow::Result<(IPCServer, String)> {
|
||||
let (oneshot_server, oneshot_server_name) = ipc::IpcOneShotServer::new()?;
|
||||
Ok((
|
||||
IPCServer {
|
||||
oneshot_server: Some(oneshot_server),
|
||||
tx: None,
|
||||
},
|
||||
oneshot_server_name,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn setup_ipc_receiver(name: &str) -> anyhow::Result<IpcReceiver<Message>> {
|
||||
let oneshot_sender: IpcSender<IpcSender<Message>> = ipc::IpcSender::connect(name.to_string())?;
|
||||
let (tx, rx) = ipc::channel()?;
|
||||
oneshot_sender.send(tx)?;
|
||||
Ok(rx)
|
||||
}
|
||||
|
||||
pub(crate) fn setup_ipc_receiver_with_env() -> anyhow::Result<IpcReceiver<Message>> {
|
||||
let name = std::env::var("NYANPASU_EGUI_IPC_SERVER")?;
|
||||
setup_ipc_receiver(&name)
|
||||
}
|
@@ -1,2 +1,5 @@
|
||||
#![feature(trait_alias)]
|
||||
|
||||
pub mod ipc;
|
||||
mod utils;
|
||||
pub mod widget;
|
||||
|
@@ -3,3 +3,35 @@ pub mod network_statistic_small;
|
||||
|
||||
pub use network_statistic_large::NyanpasuNetworkStatisticLargeWidget;
|
||||
pub use network_statistic_small::NyanpasuNetworkStatisticSmallWidget;
|
||||
|
||||
// pub fn launch_widget<'app, T: Send + Sync + Sized, A: EframeAppCreator<'app, T>>(
|
||||
// name: &str,
|
||||
// opts: eframe::NativeOptions,
|
||||
// creator: A,
|
||||
// ) -> std::io::Result<Receiver<WidgetEvent<T>>> {
|
||||
// let (tx, rx) = mpsc::channel();
|
||||
// }
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
serde::Serialize,
|
||||
serde::Deserialize,
|
||||
specta::Type,
|
||||
Copy,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Eq,
|
||||
clap::ValueEnum,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum StatisticWidgetVariant {
|
||||
Large,
|
||||
Small,
|
||||
}
|
||||
|
||||
pub fn start_statistic_widget(size: StatisticWidgetVariant) -> eframe::Result {
|
||||
match size {
|
||||
StatisticWidgetVariant::Large => NyanpasuNetworkStatisticLargeWidget::run(),
|
||||
StatisticWidgetVariant::Small => NyanpasuNetworkStatisticSmallWidget::run(),
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,14 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use crate::ipc::Message;
|
||||
use crate::utils::svg::{render_svg_with_current_color_replace, SvgExt};
|
||||
use eframe::egui::{
|
||||
self, style::Selection, Color32, Id, Image, Layout, Margin, Rounding, Sense, Stroke, Style,
|
||||
TextureOptions, Theme, Vec2, ViewportCommand, Visuals,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
|
||||
// Presets
|
||||
const STATUS_ICON_CONTAINER_WIDTH: f32 = 20.0;
|
||||
@@ -42,7 +46,9 @@ fn setup_fonts(ctx: &egui::Context) {
|
||||
|
||||
fonts.font_data.insert(
|
||||
"Inter".to_owned(),
|
||||
egui::FontData::from_static(include_bytes!("../../assets/Inter-Regular.ttf")),
|
||||
Arc::new(egui::FontData::from_static(include_bytes!(
|
||||
"../../assets/Inter-Regular.ttf"
|
||||
))),
|
||||
);
|
||||
|
||||
fonts
|
||||
@@ -85,7 +91,8 @@ fn use_dark_purple_accent(style: &mut Style) {
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum LogoPreset {
|
||||
#[default]
|
||||
Default,
|
||||
@@ -93,30 +100,20 @@ pub enum LogoPreset {
|
||||
Tun,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct StatisticMessage {
|
||||
download_total: u64,
|
||||
upload_total: u64,
|
||||
download_speed: u64,
|
||||
upload_speed: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Message {
|
||||
UpdateStatistic(StatisticMessage),
|
||||
UpdateLogo(LogoPreset),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NyanpasuNetworkStatisticLargeWidget {
|
||||
pub struct NyanpasuNetworkStatisticLargeWidgetInner {
|
||||
// data fields
|
||||
logo_preset: LogoPreset,
|
||||
download_total: u64,
|
||||
upload_total: u64,
|
||||
download_speed: u64,
|
||||
upload_speed: u64,
|
||||
|
||||
// eframe ctx
|
||||
egui_ctx: OnceLock<egui::Context>,
|
||||
}
|
||||
|
||||
impl Default for NyanpasuNetworkStatisticLargeWidget {
|
||||
impl Default for NyanpasuNetworkStatisticLargeWidgetInner {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
logo_preset: LogoPreset::Default,
|
||||
@@ -124,6 +121,22 @@ impl Default for NyanpasuNetworkStatisticLargeWidget {
|
||||
upload_total: 0,
|
||||
download_speed: 0,
|
||||
upload_speed: 0,
|
||||
egui_ctx: OnceLock::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NyanpasuNetworkStatisticLargeWidget {
|
||||
inner: Arc<RwLock<NyanpasuNetworkStatisticLargeWidgetInner>>,
|
||||
}
|
||||
|
||||
impl Default for NyanpasuNetworkStatisticLargeWidget {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(RwLock::new(
|
||||
NyanpasuNetworkStatisticLargeWidgetInner::default(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,23 +147,72 @@ impl NyanpasuNetworkStatisticLargeWidget {
|
||||
setup_fonts(&cc.egui_ctx);
|
||||
setup_custom_style(&cc.egui_ctx);
|
||||
egui_extras::install_image_loaders(&cc.egui_ctx);
|
||||
Self::default()
|
||||
let rx = crate::ipc::setup_ipc_receiver_with_env().unwrap();
|
||||
let widget = Self::default();
|
||||
let this = widget.clone();
|
||||
std::thread::spawn(move || loop {
|
||||
match rx.recv() {
|
||||
Ok(msg) => {
|
||||
let _ = this.handle_message(msg);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to receive message: {}", e);
|
||||
if matches!(
|
||||
e,
|
||||
ipc_channel::ipc::IpcError::Disconnected
|
||||
| ipc_channel::ipc::IpcError::Io(_)
|
||||
) {
|
||||
let _ = this.handle_message(Message::Stop);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
widget
|
||||
}
|
||||
|
||||
pub fn handle_message(&mut self, msg: Message) -> bool {
|
||||
pub fn run() -> eframe::Result {
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_inner_size([206.0, 60.0])
|
||||
.with_decorations(false)
|
||||
.with_transparent(true)
|
||||
.with_always_on_top()
|
||||
.with_drag_and_drop(true)
|
||||
.with_resizable(false)
|
||||
.with_taskbar(false),
|
||||
..Default::default()
|
||||
};
|
||||
eframe::run_native(
|
||||
"Nyanpasu Network Statistic Widget",
|
||||
options,
|
||||
Box::new(|cc| Ok(Box::new(NyanpasuNetworkStatisticLargeWidget::new(cc)))),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn handle_message(&self, msg: Message) -> anyhow::Result<()> {
|
||||
let mut this = self.inner.write();
|
||||
match msg {
|
||||
Message::UpdateStatistic(statistic) => {
|
||||
self.download_total = statistic.download_total;
|
||||
self.upload_total = statistic.upload_total;
|
||||
self.download_speed = statistic.download_speed;
|
||||
self.upload_speed = statistic.upload_speed;
|
||||
true
|
||||
this.download_total = statistic.download_total;
|
||||
this.upload_total = statistic.upload_total;
|
||||
this.download_speed = statistic.download_speed;
|
||||
this.upload_speed = statistic.upload_speed;
|
||||
}
|
||||
Message::UpdateLogo(logo_preset) => {
|
||||
self.logo_preset = logo_preset;
|
||||
true
|
||||
this.logo_preset = logo_preset;
|
||||
}
|
||||
Message::Stop => match this.egui_ctx.get() {
|
||||
Some(ctx) => {
|
||||
ctx.send_viewport_cmd(ViewportCommand::Close);
|
||||
}
|
||||
None => {
|
||||
eprintln!("Failed to close the widget: eframe context is not initialized");
|
||||
std::process::exit(1);
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +222,11 @@ impl eframe::App for NyanpasuNetworkStatisticLargeWidget {
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
// setup ctx
|
||||
let egui_ctx = ctx.clone();
|
||||
let this = self.inner.read();
|
||||
let _ = this.egui_ctx.get_or_init(move || egui_ctx);
|
||||
|
||||
let visuals = &ctx.style().visuals;
|
||||
|
||||
egui::CentralPanel::default()
|
||||
@@ -229,7 +296,7 @@ impl eframe::App for NyanpasuNetworkStatisticLargeWidget {
|
||||
let width = ui.available_width();
|
||||
let height = ui.available_height();
|
||||
ui.allocate_ui_with_layout(egui::Vec2::new(width, height), Layout::centered_and_justified(egui::Direction::TopDown), |ui| {
|
||||
ui.label(humansize::format_size(self.download_total, humansize::DECIMAL));
|
||||
ui.label(humansize::format_size(this.download_total, humansize::DECIMAL));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -262,7 +329,7 @@ impl eframe::App for NyanpasuNetworkStatisticLargeWidget {
|
||||
let width = ui.available_width();
|
||||
let height = ui.available_height();
|
||||
ui.allocate_ui_with_layout(egui::Vec2::new(width, height), Layout::centered_and_justified(egui::Direction::TopDown), |ui| {
|
||||
ui.label(format!("{}/s", humansize::format_size(self.download_speed, humansize::DECIMAL)));
|
||||
ui.label(format!("{}/s", humansize::format_size(this.download_speed, humansize::DECIMAL)));
|
||||
});
|
||||
});
|
||||
})
|
||||
@@ -300,7 +367,7 @@ impl eframe::App for NyanpasuNetworkStatisticLargeWidget {
|
||||
let width = ui.available_width();
|
||||
let height = ui.available_height();
|
||||
ui.allocate_ui_with_layout(egui::Vec2::new(width, height), Layout::centered_and_justified(egui::Direction::TopDown), |ui| {
|
||||
ui.label(humansize::format_size(self.upload_total, humansize::DECIMAL));
|
||||
ui.label(humansize::format_size(this.upload_total, humansize::DECIMAL));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -333,7 +400,7 @@ impl eframe::App for NyanpasuNetworkStatisticLargeWidget {
|
||||
let width = ui.available_width();
|
||||
let height = ui.available_height();
|
||||
ui.allocate_ui_with_layout(egui::Vec2::new(width, height), Layout::centered_and_justified(egui::Direction::TopDown), |ui| {
|
||||
ui.label(format!("{}/s", humansize::format_size(self.upload_speed, humansize::DECIMAL)));
|
||||
ui.label(format!("{}/s", humansize::format_size(this.upload_speed, humansize::DECIMAL)));
|
||||
});
|
||||
});
|
||||
})
|
||||
|
@@ -1,9 +1,14 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use eframe::egui::{
|
||||
self, include_image, style::Selection, Color32, Id, Image, Layout, Margin, RichText, Rounding,
|
||||
Sense, Stroke, Style, Theme, Vec2, ViewportCommand, Visuals, WidgetText,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
|
||||
use crate::ipc::Message;
|
||||
|
||||
// Presets
|
||||
const STATUS_ICON_CONTAINER_WIDTH: f32 = 20.0;
|
||||
@@ -36,7 +41,9 @@ fn setup_fonts(ctx: &egui::Context) {
|
||||
|
||||
fonts.font_data.insert(
|
||||
"Inter".to_owned(),
|
||||
egui::FontData::from_static(include_bytes!("../../assets/Inter-Regular.ttf")),
|
||||
Arc::new(egui::FontData::from_static(include_bytes!(
|
||||
"../../assets/Inter-Regular.ttf"
|
||||
))),
|
||||
);
|
||||
|
||||
fonts
|
||||
@@ -79,19 +86,105 @@ fn use_dark_purple_accent(style: &mut Style) {
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NyanpasuNetworkStatisticSmallWidget {
|
||||
demo_size: u64,
|
||||
state: Arc<RwLock<NyanpasuNetworkStatisticSmallWidgetState>>,
|
||||
}
|
||||
|
||||
impl Default for NyanpasuNetworkStatisticSmallWidget {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: Arc::new(RwLock::new(
|
||||
NyanpasuNetworkStatisticSmallWidgetState::default(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct NyanpasuNetworkStatisticSmallWidgetState {
|
||||
// data fields
|
||||
// download_total: u64,
|
||||
// upload_total: u64,
|
||||
download_speed: u64,
|
||||
upload_speed: u64,
|
||||
|
||||
// eframe ctx
|
||||
egui_ctx: OnceLock<egui::Context>,
|
||||
}
|
||||
|
||||
impl NyanpasuNetworkStatisticSmallWidget {
|
||||
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||
cc.egui_ctx.set_visuals(Visuals::light());
|
||||
cc.egui_ctx.set_visuals(Visuals::dark());
|
||||
setup_fonts(&cc.egui_ctx);
|
||||
setup_custom_style(&cc.egui_ctx);
|
||||
egui_extras::install_image_loaders(&cc.egui_ctx);
|
||||
Self {
|
||||
demo_size: 100_000_000,
|
||||
let rx = crate::ipc::setup_ipc_receiver_with_env().unwrap();
|
||||
let widget = Self::default();
|
||||
let this = widget.clone();
|
||||
std::thread::spawn(move || loop {
|
||||
match rx.recv() {
|
||||
Ok(msg) => {
|
||||
let _ = this.handle_message(msg);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to receive message: {}", e);
|
||||
if matches!(
|
||||
e,
|
||||
ipc_channel::ipc::IpcError::Disconnected
|
||||
| ipc_channel::ipc::IpcError::Io(_)
|
||||
) {
|
||||
let _ = this.handle_message(Message::Stop);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
widget
|
||||
}
|
||||
|
||||
pub fn run() -> eframe::Result {
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_inner_size([206.0, 60.0])
|
||||
.with_decorations(false)
|
||||
.with_transparent(true)
|
||||
.with_always_on_top()
|
||||
.with_drag_and_drop(true)
|
||||
.with_resizable(false)
|
||||
.with_taskbar(false),
|
||||
..Default::default()
|
||||
};
|
||||
eframe::run_native(
|
||||
"Nyanpasu Network Statistic Widget",
|
||||
options,
|
||||
Box::new(|cc| Ok(Box::new(NyanpasuNetworkStatisticSmallWidget::new(cc)))),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn handle_message(&self, msg: Message) -> anyhow::Result<()> {
|
||||
let mut this = self.state.write();
|
||||
match msg {
|
||||
Message::UpdateStatistic(statistic) => {
|
||||
// this.download_total = statistic.download_total;
|
||||
// this.upload_total = statistic.upload_total;
|
||||
this.download_speed = statistic.download_speed;
|
||||
this.upload_speed = statistic.upload_speed;
|
||||
}
|
||||
Message::Stop => match this.egui_ctx.get() {
|
||||
Some(ctx) => {
|
||||
ctx.send_viewport_cmd(ViewportCommand::Close);
|
||||
}
|
||||
None => {
|
||||
eprintln!("Failed to close the widget: eframe context is not initialized");
|
||||
std::process::exit(1);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
eprintln!("Unsupported message: {:?}", msg);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +195,9 @@ impl eframe::App for NyanpasuNetworkStatisticSmallWidget {
|
||||
|
||||
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
let visuals = &ctx.style().visuals;
|
||||
let egui_ctx = ctx.clone();
|
||||
let this = self.state.read();
|
||||
let _ = this.egui_ctx.get_or_init(move || egui_ctx);
|
||||
|
||||
egui::CentralPanel::default()
|
||||
.frame(
|
||||
@@ -153,7 +249,10 @@ impl eframe::App for NyanpasuNetworkStatisticSmallWidget {
|
||||
ui.label(
|
||||
WidgetText::from(RichText::new(format!(
|
||||
"{}/s",
|
||||
humansize::format_size(self.demo_size, humansize::DECIMAL)
|
||||
humansize::format_size(
|
||||
this.upload_speed,
|
||||
humansize::DECIMAL
|
||||
)
|
||||
)))
|
||||
.color(LIGHT_MODE_TEXT_COLOR),
|
||||
);
|
||||
@@ -166,7 +265,10 @@ impl eframe::App for NyanpasuNetworkStatisticSmallWidget {
|
||||
ui.label(
|
||||
WidgetText::from(RichText::new(format!(
|
||||
"{}/s",
|
||||
humansize::format_size(self.demo_size, humansize::DECIMAL)
|
||||
humansize::format_size(
|
||||
this.download_speed,
|
||||
humansize::DECIMAL
|
||||
)
|
||||
)))
|
||||
.color(LIGHT_MODE_TEXT_COLOR),
|
||||
);
|
||||
|
@@ -16,7 +16,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.1", features = [] }
|
||||
serde = "1"
|
||||
simd-json = "0.14.1"
|
||||
serde_json = { version = "1.0", features = ["preserve_order"] }
|
||||
chrono = "0.4"
|
||||
rustc_version = "0.4"
|
||||
semver = "1.0"
|
||||
@@ -31,10 +31,12 @@ nyanpasu-macro = { path = "../nyanpasu-macro" }
|
||||
nyanpasu-utils = { git = "https://github.com/libnyanpasu/nyanpasu-utils.git", features = [
|
||||
"specta",
|
||||
] }
|
||||
nyanpasu-egui = { path = "../nyanpasu-egui" }
|
||||
|
||||
# Common Utilities
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
glob = "0.3.1"
|
||||
timeago = "0.4"
|
||||
humansize = "2.1.3"
|
||||
@@ -47,13 +49,14 @@ async-trait = "0.1.77"
|
||||
dyn-clone = "1.0.16"
|
||||
thiserror = { workspace = true }
|
||||
parking_lot = { version = "0.12.1" }
|
||||
itertools = "0.14" # sweet iterator utilities
|
||||
rayon = "1.10" # for iterator parallel processing
|
||||
ambassador = "0.4.1" # for trait delegation
|
||||
derive_builder = "0.20" # for builder pattern
|
||||
strum = { version = "0.26", features = ["derive"] } # for enum string conversion
|
||||
atomic_enum = "0.3.0" # for atomic enum
|
||||
enumflags2 = "0.7" # for enum flags
|
||||
itertools = "0.14" # sweet iterator utilities
|
||||
rayon = "1.10" # for iterator parallel processing
|
||||
ambassador = "0.4.1" # for trait delegation
|
||||
derive_builder = "0.20" # for builder pattern
|
||||
strum = { version = "0.26", features = ["derive"] } # for enum string conversion
|
||||
atomic_enum = "0.3.0" # for atomic enum
|
||||
enumflags2 = "0.7" # for enum flags
|
||||
backon = { version = "1.0.1", features = ["tokio-sleep"] } # for backoff retry
|
||||
|
||||
# Data Structures
|
||||
dashmap = "6"
|
||||
@@ -61,7 +64,7 @@ indexmap = { version = "2.2.3", features = ["serde"] }
|
||||
bimap = "0.6.3"
|
||||
|
||||
# Terminal Utilities
|
||||
ansi-str = "0.8" # for ansi str stripped
|
||||
ansi-str = "0.9" # for ansi str stripped
|
||||
ctrlc = "3.4.2"
|
||||
colored = "3"
|
||||
clap = { version = "4.5.4", features = ["derive"] }
|
||||
@@ -81,17 +84,17 @@ axum = "0.8"
|
||||
url = "2"
|
||||
mime = "0.3"
|
||||
reqwest = { version = "0.12", features = ["json", "stream"] }
|
||||
tokio-tungstenite = "0.26.1"
|
||||
urlencoding = "2.1"
|
||||
port_scanner = "0.1.5"
|
||||
sysproxy = { git = "https://github.com/libnyanpasu/sysproxy-rs.git", version = "0.3" }
|
||||
backon = { version = "1.0.1", features = ["tokio-sleep"] }
|
||||
|
||||
# Serialization
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_json = { version = "1.0", features = ["preserve_order"] }
|
||||
serde_yaml = { version = "0.10", package = "serde_yaml_ng", branch = "feat/specta", git = "https://github.com/libnyanpasu/serde-yaml-ng.git", features = [
|
||||
"specta",
|
||||
] }
|
||||
simd-json = "0.14.1"
|
||||
bincode = "1"
|
||||
bytes = { version = "1", features = ["serde"] }
|
||||
semver = "1.0"
|
||||
|
@@ -20,12 +20,12 @@ struct GitInfo {
|
||||
|
||||
fn main() {
|
||||
let version: String = if let Ok(true) = exists("../../package.json") {
|
||||
let mut raw = read("../../package.json").unwrap();
|
||||
let pkg_json: PackageJson = simd_json::from_slice(&mut raw).unwrap();
|
||||
let raw = read("../../package.json").unwrap();
|
||||
let pkg_json: PackageJson = serde_json::from_slice(&raw).unwrap();
|
||||
pkg_json.version
|
||||
} else {
|
||||
let mut raw = read("./tauri.conf.json").unwrap(); // TODO: fix it when windows arm64 need it
|
||||
let tauri_json: PackageJson = simd_json::from_slice(&mut raw).unwrap();
|
||||
let raw = read("./tauri.conf.json").unwrap(); // TODO: fix it when windows arm64 need it
|
||||
let tauri_json: PackageJson = serde_json::from_slice(&raw).unwrap();
|
||||
tauri_json.version
|
||||
};
|
||||
let version = semver::Version::parse(&version).unwrap();
|
||||
@@ -34,8 +34,8 @@ fn main() {
|
||||
// Git Information
|
||||
let (commit_hash, commit_author, commit_date) = if let Ok(true) = exists("./tmp/git-info.json")
|
||||
{
|
||||
let mut git_info = read("./tmp/git-info.json").unwrap();
|
||||
let git_info: GitInfo = simd_json::from_slice(&mut git_info).unwrap();
|
||||
let git_info = read("./tmp/git-info.json").unwrap();
|
||||
let git_info: GitInfo = serde_json::from_slice(&git_info).unwrap();
|
||||
(git_info.hash, git_info.author, git_info.time)
|
||||
} else {
|
||||
let output = Command::new("git")
|
||||
|
File diff suppressed because one or more lines are too long
@@ -1724,6 +1724,14 @@
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
},
|
||||
"cmd": {
|
||||
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
|
||||
"type": "string"
|
||||
},
|
||||
"args": {
|
||||
"description": "The allowed arguments for the command execution.",
|
||||
"allOf": [
|
||||
@@ -1731,14 +1739,6 @@
|
||||
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cmd": {
|
||||
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -1750,6 +1750,10 @@
|
||||
"sidecar"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
},
|
||||
"args": {
|
||||
"description": "The allowed arguments for the command execution.",
|
||||
"allOf": [
|
||||
@@ -1758,10 +1762,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
},
|
||||
"sidecar": {
|
||||
"description": "If this command is a sidecar command.",
|
||||
"type": "boolean"
|
||||
@@ -1784,6 +1784,14 @@
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
},
|
||||
"cmd": {
|
||||
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
|
||||
"type": "string"
|
||||
},
|
||||
"args": {
|
||||
"description": "The allowed arguments for the command execution.",
|
||||
"allOf": [
|
||||
@@ -1791,14 +1799,6 @@
|
||||
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cmd": {
|
||||
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -1810,6 +1810,10 @@
|
||||
"sidecar"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
},
|
||||
"args": {
|
||||
"description": "The allowed arguments for the command execution.",
|
||||
"allOf": [
|
||||
@@ -1818,10 +1822,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
},
|
||||
"sidecar": {
|
||||
"description": "If this command is a sidecar command.",
|
||||
"type": "boolean"
|
||||
@@ -5503,34 +5503,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ShellScopeEntryAllowedArg": {
|
||||
"description": "A command argument allowed to be executed by the webview API.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "A non-configurable argument that is passed to the command in the order it was specified.",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "A variable that is set while calling the command from the webview API.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"validator"
|
||||
],
|
||||
"properties": {
|
||||
"raw": {
|
||||
"description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.",
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"validator": {
|
||||
"description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: <https://docs.rs/regex/latest/regex/#syntax>",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"ShellScopeEntryAllowedArgs": {
|
||||
"description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.",
|
||||
"anyOf": [
|
||||
@@ -5546,6 +5518,34 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ShellScopeEntryAllowedArg": {
|
||||
"description": "A command argument allowed to be executed by the webview API.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "A non-configurable argument that is passed to the command in the order it was specified.",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "A variable that is set while calling the command from the webview API.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"validator"
|
||||
],
|
||||
"properties": {
|
||||
"validator": {
|
||||
"description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: <https://docs.rs/regex/latest/regex/#syntax>",
|
||||
"type": "string"
|
||||
},
|
||||
"raw": {
|
||||
"description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.",
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@@ -1724,6 +1724,14 @@
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
},
|
||||
"cmd": {
|
||||
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
|
||||
"type": "string"
|
||||
},
|
||||
"args": {
|
||||
"description": "The allowed arguments for the command execution.",
|
||||
"allOf": [
|
||||
@@ -1731,14 +1739,6 @@
|
||||
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cmd": {
|
||||
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -1750,6 +1750,10 @@
|
||||
"sidecar"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
},
|
||||
"args": {
|
||||
"description": "The allowed arguments for the command execution.",
|
||||
"allOf": [
|
||||
@@ -1758,10 +1762,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
},
|
||||
"sidecar": {
|
||||
"description": "If this command is a sidecar command.",
|
||||
"type": "boolean"
|
||||
@@ -1784,6 +1784,14 @@
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
},
|
||||
"cmd": {
|
||||
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
|
||||
"type": "string"
|
||||
},
|
||||
"args": {
|
||||
"description": "The allowed arguments for the command execution.",
|
||||
"allOf": [
|
||||
@@ -1791,14 +1799,6 @@
|
||||
"$ref": "#/definitions/ShellScopeEntryAllowedArgs"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cmd": {
|
||||
"description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -1810,6 +1810,10 @@
|
||||
"sidecar"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
},
|
||||
"args": {
|
||||
"description": "The allowed arguments for the command execution.",
|
||||
"allOf": [
|
||||
@@ -1818,10 +1822,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.",
|
||||
"type": "string"
|
||||
},
|
||||
"sidecar": {
|
||||
"description": "If this command is a sidecar command.",
|
||||
"type": "boolean"
|
||||
@@ -5503,34 +5503,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ShellScopeEntryAllowedArg": {
|
||||
"description": "A command argument allowed to be executed by the webview API.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "A non-configurable argument that is passed to the command in the order it was specified.",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "A variable that is set while calling the command from the webview API.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"validator"
|
||||
],
|
||||
"properties": {
|
||||
"raw": {
|
||||
"description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.",
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"validator": {
|
||||
"description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: <https://docs.rs/regex/latest/regex/#syntax>",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"ShellScopeEntryAllowedArgs": {
|
||||
"description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.",
|
||||
"anyOf": [
|
||||
@@ -5546,6 +5518,34 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ShellScopeEntryAllowedArg": {
|
||||
"description": "A command argument allowed to be executed by the webview API.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "A non-configurable argument that is passed to the command in the order it was specified.",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "A variable that is set while calling the command from the webview API.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"validator"
|
||||
],
|
||||
"properties": {
|
||||
"validator": {
|
||||
"description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: <https://docs.rs/regex/latest/regex/#syntax>",
|
||||
"type": "string"
|
||||
},
|
||||
"raw": {
|
||||
"description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.",
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,12 +1,12 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::utils;
|
||||
use anyhow::Ok;
|
||||
use clap::{Parser, Subcommand};
|
||||
use migrate::MigrateOpts;
|
||||
use nyanpasu_egui::widget::StatisticWidgetVariant;
|
||||
use tauri::utils::platform::current_exe;
|
||||
|
||||
use crate::utils;
|
||||
|
||||
mod migrate;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
@@ -38,6 +38,8 @@ enum Commands {
|
||||
},
|
||||
/// Show a panic dialog while the application is enter panic handler.
|
||||
PanicDialog { message: String },
|
||||
/// Launch the Widget with the specified name.
|
||||
StatisticWidget { variant: StatisticWidgetVariant },
|
||||
}
|
||||
|
||||
struct DelayedExitGuard;
|
||||
@@ -92,6 +94,10 @@ pub fn parse() -> anyhow::Result<()> {
|
||||
Commands::PanicDialog { message } => {
|
||||
crate::utils::dialog::panic_dialog(message);
|
||||
}
|
||||
Commands::StatisticWidget { variant } => {
|
||||
nyanpasu_egui::widget::start_statistic_widget(*variant)
|
||||
.expect("Failed to start statistic widget");
|
||||
}
|
||||
}
|
||||
drop(guard);
|
||||
std::process::exit(0);
|
||||
|
@@ -8,9 +8,11 @@ use specta::Type;
|
||||
|
||||
mod clash_strategy;
|
||||
pub mod logging;
|
||||
mod widget;
|
||||
|
||||
pub use self::clash_strategy::{ClashStrategy, ExternalControllerPortStrategy};
|
||||
pub use logging::LoggingLevel;
|
||||
pub use widget::NetworkStatisticWidgetConfig;
|
||||
|
||||
// TODO: when support sing-box, remove this struct
|
||||
#[bitflags]
|
||||
@@ -132,6 +134,7 @@ impl AsRef<str> for TunStack {
|
||||
/// ### `verge.yaml` schema
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, VergePatch, specta::Type)]
|
||||
#[verge(patch_fn = "patch_config")]
|
||||
// TODO: use new managedState and builder pattern instead
|
||||
pub struct IVerge {
|
||||
/// app listening port for app singleton
|
||||
pub app_singleton_port: Option<u16>,
|
||||
@@ -248,6 +251,10 @@ pub struct IVerge {
|
||||
/// Tun 堆栈选择
|
||||
/// TODO: 弃用此字段,转移到 clash config 里
|
||||
pub tun_stack: Option<TunStack>,
|
||||
|
||||
/// 是否启用网络统计信息浮窗
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub network_statistic_widget: Option<NetworkStatisticWidgetConfig>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, Type)]
|
||||
|
12
clash-nyanpasu/backend/tauri/src/config/nyanpasu/widget.rs
Normal file
12
clash-nyanpasu/backend/tauri/src/config/nyanpasu/widget.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use nyanpasu_egui::widget::StatisticWidgetVariant;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Type)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[serde(tag = "kind", content = "value")]
|
||||
pub enum NetworkStatisticWidgetConfig {
|
||||
#[default]
|
||||
Disabled,
|
||||
Enabled(StatisticWidgetVariant),
|
||||
}
|
92
clash-nyanpasu/backend/tauri/src/config/profile/builder.rs
Normal file
92
clash-nyanpasu/backend/tauri/src/config/profile/builder.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use crate::config::profile::item_type::ProfileItemType;
|
||||
|
||||
use super::item::{
|
||||
LocalProfileBuilder, MergeProfileBuilder, RemoteProfileBuilder, ScriptProfileBuilder,
|
||||
};
|
||||
use serde::{de::Visitor, Deserialize, Deserializer, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, specta::Type)]
|
||||
#[serde(untagged)]
|
||||
pub enum ProfileBuilder {
|
||||
Remote(RemoteProfileBuilder),
|
||||
Local(LocalProfileBuilder),
|
||||
Merge(MergeProfileBuilder),
|
||||
Script(ScriptProfileBuilder),
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ProfileBuilder {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct ProfileBuilderVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ProfileBuilderVisitor {
|
||||
type Value = ProfileBuilder;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("expecting a profile builder, possible values a map with a key of `type` and a value of `remote`, `local`, `merge`, or `script`")
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
let mut mapping: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::Map::new();
|
||||
let mut type_field = None;
|
||||
while let Some((key, value)) = map.next_entry::<String, serde_json::Value>()? {
|
||||
if "type" == key.as_str() {
|
||||
tracing::debug!("type field: {:#?}", value);
|
||||
type_field =
|
||||
Some(ProfileItemType::deserialize(value.clone()).map_err(|err| {
|
||||
serde::de::Error::custom(format!(
|
||||
"failed to deserialize profile builder type: {}",
|
||||
err
|
||||
))
|
||||
})?);
|
||||
}
|
||||
mapping.insert(key, value);
|
||||
}
|
||||
let type_field =
|
||||
type_field.ok_or_else(|| serde::de::Error::missing_field("type"))?;
|
||||
match type_field {
|
||||
ProfileItemType::Remote => RemoteProfileBuilder::deserialize(mapping)
|
||||
.map(ProfileBuilder::Remote)
|
||||
.map_err(|err| {
|
||||
serde::de::Error::custom(format!(
|
||||
"failed to deserialize remote profile builder: {}",
|
||||
err
|
||||
))
|
||||
}),
|
||||
ProfileItemType::Local => LocalProfileBuilder::deserialize(mapping)
|
||||
.map(ProfileBuilder::Local)
|
||||
.map_err(|err| {
|
||||
serde::de::Error::custom(format!(
|
||||
"failed to deserialize local profile builder: {}",
|
||||
err
|
||||
))
|
||||
}),
|
||||
ProfileItemType::Merge => MergeProfileBuilder::deserialize(mapping)
|
||||
.map(ProfileBuilder::Merge)
|
||||
.map_err(|err| {
|
||||
serde::de::Error::custom(format!(
|
||||
"failed to deserialize merge profile builder: {}",
|
||||
err
|
||||
))
|
||||
}),
|
||||
ProfileItemType::Script(_) => ScriptProfileBuilder::deserialize(mapping)
|
||||
.map(ProfileBuilder::Script)
|
||||
.map_err(|err| {
|
||||
serde::de::Error::custom(format!(
|
||||
"failed to deserialize script profile builder: {}",
|
||||
err
|
||||
))
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_map(ProfileBuilderVisitor)
|
||||
}
|
||||
}
|
@@ -94,6 +94,7 @@ pub trait ProfileCleanup: ProfileHelper {
|
||||
#[delegate(ProfileSharedSetter)]
|
||||
#[delegate(ProfileSharedGetter)]
|
||||
#[delegate(ProfileFileIo)]
|
||||
#[specta(untagged)]
|
||||
pub enum Profile {
|
||||
Remote(RemoteProfile),
|
||||
Local(LocalProfile),
|
||||
|
@@ -1,4 +1,6 @@
|
||||
pub mod builder;
|
||||
pub mod item;
|
||||
pub mod item_type;
|
||||
pub mod profiles;
|
||||
pub use builder::ProfileBuilder;
|
||||
use item::deserialize_single_or_vec;
|
||||
|
@@ -1,8 +1,6 @@
|
||||
use super::{
|
||||
item::{
|
||||
prelude::*, LocalProfileBuilder, MergeProfileBuilder, Profile, RemoteProfileBuilder,
|
||||
ScriptProfileBuilder,
|
||||
},
|
||||
builder::ProfileBuilder,
|
||||
item::{prelude::*, Profile},
|
||||
item_type::ProfileUid,
|
||||
};
|
||||
use crate::utils::{dirs, help};
|
||||
@@ -16,14 +14,6 @@ use serde_yaml::Mapping;
|
||||
use std::borrow::Borrow;
|
||||
use tracing_attributes::instrument;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, specta::Type)]
|
||||
pub enum ProfileKind {
|
||||
Remote(RemoteProfileBuilder),
|
||||
Local(LocalProfileBuilder),
|
||||
Merge(MergeProfileBuilder),
|
||||
Script(ScriptProfileBuilder),
|
||||
}
|
||||
|
||||
/// Define the `profiles.yaml` schema
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Builder, BuilderUpdate, specta::Type)]
|
||||
#[builder(derive(Serialize, Deserialize, specta::Type))]
|
||||
@@ -140,7 +130,7 @@ impl Profiles {
|
||||
|
||||
/// update the item value
|
||||
#[instrument]
|
||||
pub fn patch_item(&mut self, uid: String, patch: ProfileKind) -> Result<()> {
|
||||
pub fn patch_item(&mut self, uid: String, patch: ProfileBuilder) -> Result<()> {
|
||||
tracing::debug!("patch item: {uid} with {patch:?}");
|
||||
|
||||
let item = self
|
||||
@@ -154,10 +144,10 @@ impl Profiles {
|
||||
tracing::debug!("patch item: {item:?}");
|
||||
|
||||
match (item, patch) {
|
||||
(Profile::Remote(item), ProfileKind::Remote(builder)) => item.apply(builder),
|
||||
(Profile::Local(item), ProfileKind::Local(builder)) => item.apply(builder),
|
||||
(Profile::Merge(item), ProfileKind::Merge(builder)) => item.apply(builder),
|
||||
(Profile::Script(item), ProfileKind::Script(builder)) => item.apply(builder),
|
||||
(Profile::Remote(item), ProfileBuilder::Remote(builder)) => item.apply(builder),
|
||||
(Profile::Local(item), ProfileBuilder::Local(builder)) => item.apply(builder),
|
||||
(Profile::Merge(item), ProfileBuilder::Merge(builder)) => item.apply(builder),
|
||||
(Profile::Script(item), ProfileBuilder::Script(builder)) => item.apply(builder),
|
||||
_ => bail!("profile type mismatch when patching"),
|
||||
};
|
||||
|
||||
|
@@ -49,3 +49,7 @@ static APP_HANDLE: OnceCell<AppHandle> = OnceCell::new();
|
||||
pub fn app_handle() -> &'static AppHandle {
|
||||
APP_HANDLE.get().expect("app handle not initialized")
|
||||
}
|
||||
|
||||
pub(super) fn setup_app_handle(app_handle: AppHandle) {
|
||||
let _ = APP_HANDLE.set(app_handle);
|
||||
}
|
||||
|
@@ -1,8 +1,13 @@
|
||||
use backon::ExponentialBuilder;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use tauri_specta::Event;
|
||||
|
||||
pub mod api;
|
||||
pub mod core;
|
||||
pub mod proxies;
|
||||
pub mod ws;
|
||||
|
||||
pub static CLASH_API_DEFAULT_BACKOFF_STRATEGY: Lazy<ExponentialBuilder> = Lazy::new(|| {
|
||||
ExponentialBuilder::default()
|
||||
@@ -10,3 +15,25 @@ pub static CLASH_API_DEFAULT_BACKOFF_STRATEGY: Lazy<ExponentialBuilder> = Lazy::
|
||||
.with_max_delay(std::time::Duration::from_secs(5))
|
||||
.with_max_times(5)
|
||||
});
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Type, Event)]
|
||||
pub struct ClashConnectionsEvent(pub ws::ClashConnectionsConnectorEvent);
|
||||
|
||||
pub fn setup<R: tauri::Runtime, M: tauri::Manager<R>>(manager: &M) -> anyhow::Result<()> {
|
||||
let ws_connector = ws::ClashConnectionsConnector::new();
|
||||
manager.manage(ws_connector.clone());
|
||||
let app_handle = manager.app_handle().clone();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// TODO: refactor it while clash core manager use tauri event dispatcher to notify the core state changed
|
||||
{
|
||||
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
|
||||
ws_connector.start().await.unwrap();
|
||||
}
|
||||
let mut rx = ws_connector.subscribe();
|
||||
while let Ok(event) = rx.recv().await {
|
||||
ClashConnectionsEvent(event).emit(&app_handle).unwrap();
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
@@ -261,7 +261,7 @@ type ProxiesGuardSingleton = &'static Arc<RwLock<ProxiesGuard>>;
|
||||
impl ProxiesGuardExt for ProxiesGuardSingleton {
|
||||
async fn update(&self) -> Result<()> {
|
||||
let proxies = Proxies::fetch().await?;
|
||||
let buf = simd_json::to_string(&proxies)?;
|
||||
let buf = serde_json::to_string(&proxies)?;
|
||||
let checksum = adler32(buf.as_bytes())?;
|
||||
{
|
||||
let reader = self.read();
|
||||
|
@@ -0,0 +1,286 @@
|
||||
use std::{
|
||||
future::Future,
|
||||
ops::Deref,
|
||||
sync::{atomic::Ordering, Arc},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use atomic_enum::atomic_enum;
|
||||
use backon::Retryable;
|
||||
use futures_util::StreamExt;
|
||||
use parking_lot::Mutex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use tokio::{sync::mpsc::Receiver, task::JoinHandle};
|
||||
use tokio_tungstenite::{
|
||||
connect_async,
|
||||
tungstenite::{client::IntoClientRequest, handshake::client::Request, protocol::Message},
|
||||
};
|
||||
|
||||
use crate::log_err;
|
||||
|
||||
#[tracing::instrument]
|
||||
async fn connect_clash_server<T: serde::de::DeserializeOwned + Send + Sync + 'static>(
|
||||
endpoint: Request,
|
||||
) -> anyhow::Result<Receiver<T>> {
|
||||
let (stream, _) = connect_async(endpoint).await?;
|
||||
let (_, mut read) = stream.split();
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(32);
|
||||
tokio::spawn(async move {
|
||||
while let Some(msg) = read.next().await {
|
||||
match msg {
|
||||
Ok(Message::Text(text)) => match serde_json::from_str(&text) {
|
||||
Ok(data) => {
|
||||
let _ = tx.send(data).await;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("failed to deserialize json: {}", e);
|
||||
}
|
||||
},
|
||||
Ok(Message::Binary(bin)) => match serde_json::from_slice(&bin) {
|
||||
Ok(data) => {
|
||||
let _ = tx.send(data).await;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("failed to deserialize json: {}", e);
|
||||
}
|
||||
},
|
||||
Ok(Message::Close(_)) => {
|
||||
tracing::info!("server closed connection");
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("failed to read message: {}", e);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(rx)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ClashConnectionsMessage {
|
||||
download_total: u64,
|
||||
upload_total: u64,
|
||||
// other fields are omitted
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Copy, Type, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ClashConnectionsInfo {
|
||||
pub download_total: u64,
|
||||
pub upload_total: u64,
|
||||
pub download_speed: u64,
|
||||
pub upload_speed: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Type, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[serde(tag = "kind", content = "data")]
|
||||
pub enum ClashConnectionsConnectorEvent {
|
||||
StateChanged(ClashConnectionsConnectorState),
|
||||
Update(ClashConnectionsInfo),
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Type, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[atomic_enum]
|
||||
pub enum ClashConnectionsConnectorState {
|
||||
Disconnected,
|
||||
Connecting,
|
||||
Connected,
|
||||
}
|
||||
|
||||
pub struct ClashConnectionsConnectorInner {
|
||||
state: AtomicClashConnectionsConnectorState,
|
||||
connection_handler: Mutex<Option<JoinHandle<()>>>,
|
||||
broadcast_tx: tokio::sync::broadcast::Sender<ClashConnectionsConnectorEvent>,
|
||||
info: Mutex<ClashConnectionsInfo>,
|
||||
}
|
||||
|
||||
// TODO:
|
||||
#[derive(Clone)]
|
||||
pub struct ClashConnectionsConnector {
|
||||
inner: Arc<ClashConnectionsConnectorInner>,
|
||||
}
|
||||
|
||||
impl Deref for ClashConnectionsConnector {
|
||||
type Target = ClashConnectionsConnectorInner;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl ClashConnectionsConnector {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(ClashConnectionsConnectorInner::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn endpoint() -> anyhow::Result<Request> {
|
||||
let (server, secret) = {
|
||||
let info = crate::Config::clash().data().get_client_info();
|
||||
(info.server, info.secret)
|
||||
};
|
||||
let url = format!("ws://{}/connections", server);
|
||||
let mut request = url
|
||||
.into_client_request()
|
||||
.context("failed to create client request")?;
|
||||
if let Some(secret) = secret {
|
||||
request.headers_mut().insert(
|
||||
"Authorization",
|
||||
format!("Bearer {}", secret)
|
||||
.parse()
|
||||
.context("failed to create header value")?,
|
||||
);
|
||||
}
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
#[allow(clippy::manual_async_fn)]
|
||||
// FIXME: move to async fn while rust new solver got merged
|
||||
// ref: https://github.com/rust-lang/rust/issues/123072
|
||||
fn start_internal(&self) -> impl Future<Output = anyhow::Result<()>> + Send + use<'_> {
|
||||
async {
|
||||
self.dispatch_state_changed(ClashConnectionsConnectorState::Connecting);
|
||||
let endpoint = Self::endpoint().context("failed to create endpoint")?;
|
||||
log::debug!("connecting to clash connections ws server: {:?}", endpoint);
|
||||
let mut rx = connect_clash_server::<ClashConnectionsMessage>(endpoint).await?;
|
||||
self.dispatch_state_changed(ClashConnectionsConnectorState::Connected);
|
||||
let this = self.clone();
|
||||
let mut connection_handler = self.connection_handler.lock();
|
||||
let handle = tokio::spawn(async move {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Some(msg) => {
|
||||
this.update(msg);
|
||||
}
|
||||
None => {
|
||||
tracing::info!("clash ws server closed connection, trying to restart");
|
||||
// The connection was closed, let's restart the connector
|
||||
this.dispatch_state_changed(
|
||||
ClashConnectionsConnectorState::Disconnected,
|
||||
);
|
||||
tokio::spawn(async move {
|
||||
let restart = async || this.restart().await;
|
||||
log_err!(restart
|
||||
.retry(backon::ExponentialBuilder::default())
|
||||
.sleep(tokio::time::sleep)
|
||||
.await
|
||||
.context("failed to restart clash connections"));
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
*connection_handler = Some(handle);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start(&self) -> anyhow::Result<()> {
|
||||
self.start_internal().await.inspect_err(|_| {
|
||||
self.dispatch_state_changed(ClashConnectionsConnectorState::Disconnected);
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn restart(&self) -> anyhow::Result<()> {
|
||||
self.stop().await;
|
||||
self.start().await
|
||||
}
|
||||
}
|
||||
|
||||
impl ClashConnectionsConnectorInner {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: AtomicClashConnectionsConnectorState::new(
|
||||
ClashConnectionsConnectorState::Disconnected,
|
||||
),
|
||||
connection_handler: Mutex::new(None),
|
||||
broadcast_tx: tokio::sync::broadcast::channel(5).0,
|
||||
info: Mutex::new(ClashConnectionsInfo::default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state(&self) -> ClashConnectionsConnectorState {
|
||||
self.state.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
fn dispatch_state_changed(&self, state: ClashConnectionsConnectorState) {
|
||||
self.state.store(state, Ordering::Release);
|
||||
// SAFETY: the failures only there no active receivers,
|
||||
// so that the message will be dropped directly
|
||||
let _ = self
|
||||
.broadcast_tx
|
||||
.send(ClashConnectionsConnectorEvent::StateChanged(state));
|
||||
}
|
||||
|
||||
/// Subscribe to the ClashConnectionsConnectorEvent
|
||||
pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<ClashConnectionsConnectorEvent> {
|
||||
self.broadcast_tx.subscribe()
|
||||
}
|
||||
|
||||
fn update(&self, msg: ClashConnectionsMessage) {
|
||||
let mut info = self.info.lock();
|
||||
let previous_download_total =
|
||||
std::mem::replace(&mut info.download_total, msg.download_total);
|
||||
let previous_upload_total = std::mem::replace(&mut info.upload_total, msg.upload_total);
|
||||
info.download_speed = msg
|
||||
.download_total
|
||||
.checked_sub(previous_download_total)
|
||||
.unwrap_or_default();
|
||||
info.upload_speed = msg
|
||||
.upload_total
|
||||
.checked_sub(previous_upload_total)
|
||||
.unwrap_or_default();
|
||||
|
||||
// SAFETY: the failures only there no active receivers,
|
||||
// so that the message will be dropped directly
|
||||
let _ = self
|
||||
.broadcast_tx
|
||||
.send(ClashConnectionsConnectorEvent::Update(*info));
|
||||
}
|
||||
|
||||
pub async fn stop(&self) {
|
||||
log::info!("stopping clash connections ws server");
|
||||
let handle = self.connection_handler.lock().take();
|
||||
if let Some(handle) = handle {
|
||||
handle.abort();
|
||||
let _ = handle.await;
|
||||
}
|
||||
self.dispatch_state_changed(ClashConnectionsConnectorState::Disconnected);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ClashConnectionsConnectorInner {
|
||||
fn drop(&mut self) {
|
||||
let cleanup = async move {
|
||||
self.stop().await;
|
||||
};
|
||||
match tokio::runtime::Handle::try_current() {
|
||||
Ok(_) => tokio::task::block_in_place(|| {
|
||||
tauri::async_runtime::block_on(cleanup);
|
||||
}),
|
||||
Err(_) => {
|
||||
tauri::async_runtime::block_on(cleanup);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_connect_clash_server() {
|
||||
"ws://127.0.0.1:12649:10808/connections"
|
||||
.into_client_request()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
@@ -347,5 +347,5 @@ pub async fn status<'a>() -> anyhow::Result<nyanpasu_ipc::types::StatusInfo<'a>>
|
||||
}
|
||||
let mut status = String::from_utf8(output.stdout)?;
|
||||
tracing::trace!("service status: {}", status);
|
||||
Ok(unsafe { simd_json::serde::from_str(&mut status)? })
|
||||
Ok(serde_json::from_str(&mut status)?)
|
||||
}
|
||||
|
@@ -101,7 +101,7 @@ where
|
||||
|
||||
/// get the current state, it will return the ManagedStateLocker for the state
|
||||
pub fn latest(&self) -> MappedRwLockReadGuard<'_, T> {
|
||||
if self.is_dirty.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
if self.is_dirty() {
|
||||
let draft = self.draft.read();
|
||||
RwLockReadGuard::map(draft, |guard| guard.as_ref().unwrap())
|
||||
} else {
|
||||
@@ -125,8 +125,8 @@ where
|
||||
self.is_dirty
|
||||
.store(true, std::sync::atomic::Ordering::Release);
|
||||
|
||||
RwLockWriteGuard::map(self.draft.write(), |guard| {
|
||||
*guard = Some(state.clone());
|
||||
RwLockWriteGuard::map(self.draft.write(), move |guard| {
|
||||
*guard = Some(state);
|
||||
guard.as_mut().unwrap()
|
||||
})
|
||||
}
|
||||
|
@@ -73,8 +73,7 @@ impl JobExt for EventsRotateJob {
|
||||
fn setup(&self) -> Option<crate::core::tasks::task::Task> {
|
||||
Some(crate::core::tasks::task::Task {
|
||||
name: CLEAR_EVENTS_TASK_NAME.to_string(),
|
||||
// 12:00 every day
|
||||
schedule: TaskSchedule::Cron("0 12 * * *".to_string()),
|
||||
schedule: TaskSchedule::Cron("@hourly".to_string()),
|
||||
executor: TaskExecutor::Async(Box::new(self.clone())),
|
||||
..Default::default()
|
||||
})
|
||||
|
@@ -33,8 +33,7 @@ impl TaskStorage {
|
||||
let value = table.get(Self::TASKS_KEY.as_bytes())?;
|
||||
match value {
|
||||
Some(value) => {
|
||||
let mut value = value.value().to_owned();
|
||||
let tasks: Vec<TaskID> = simd_json::from_slice(value.as_mut_slice())?;
|
||||
let tasks: Vec<TaskID> = serde_json::from_slice(value.value())?;
|
||||
Ok(tasks)
|
||||
}
|
||||
None => Ok(Vec::new()),
|
||||
@@ -50,14 +49,12 @@ impl TaskStorage {
|
||||
let mut tasks = table
|
||||
.get(Self::TASKS_KEY.as_bytes())?
|
||||
.and_then(|val| {
|
||||
let mut value = val.value().to_owned();
|
||||
let tasks: HashSet<TaskID> =
|
||||
simd_json::from_slice(value.as_mut_slice()).ok()?;
|
||||
let tasks: HashSet<TaskID> = serde_json::from_slice(val.value()).ok()?;
|
||||
Some(tasks)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
tasks.insert(task_id);
|
||||
let value = simd_json::to_vec(&tasks)?;
|
||||
let value = serde_json::to_vec(&tasks)?;
|
||||
table.insert(Self::TASKS_KEY.as_bytes(), value.as_slice())?;
|
||||
}
|
||||
write_txn.commit()?;
|
||||
@@ -85,8 +82,7 @@ impl TaskStorage {
|
||||
let value = table.get(key.as_bytes())?;
|
||||
match value {
|
||||
Some(value) => {
|
||||
let mut value = value.value().to_owned();
|
||||
let event: TaskEvent = simd_json::from_slice(value.as_mut_slice())?;
|
||||
let event: TaskEvent = serde_json::from_slice(value.value())?;
|
||||
Ok(Some(event))
|
||||
}
|
||||
None => Ok(None),
|
||||
@@ -116,10 +112,7 @@ impl TaskStorage {
|
||||
let table = read_txn.open_table(NYANPASU_TABLE)?;
|
||||
let value = table.get(key.as_bytes())?;
|
||||
let value: Vec<TaskEventID> = match value {
|
||||
Some(value) => {
|
||||
let mut value = value.value().to_owned();
|
||||
simd_json::from_slice(value.as_mut_slice())?
|
||||
}
|
||||
Some(value) => serde_json::from_slice(value.value())?,
|
||||
None => return Ok(None),
|
||||
};
|
||||
Ok(Some(value))
|
||||
@@ -133,8 +126,8 @@ impl TaskStorage {
|
||||
let db = self.storage.get_instance();
|
||||
let event_key = format!("task:event:id:{}", event.id);
|
||||
let event_ids_key = format!("task:events:task_id:{}", event.task_id);
|
||||
let event_value = simd_json::to_vec(event)?;
|
||||
let event_ids = simd_json::to_vec(&event_ids)?;
|
||||
let event_value = serde_json::to_vec(event)?;
|
||||
let event_ids = serde_json::to_vec(&event_ids)?;
|
||||
let write_txn = db.begin_write()?;
|
||||
{
|
||||
let mut table = write_txn.open_table(NYANPASU_TABLE)?;
|
||||
@@ -149,7 +142,7 @@ impl TaskStorage {
|
||||
pub fn update_event(&self, event: &TaskEvent) -> Result<()> {
|
||||
let db = self.storage.get_instance();
|
||||
let event_key = format!("task:event:id:{}", event.id);
|
||||
let event_value = simd_json::to_vec(event)?;
|
||||
let event_value = serde_json::to_vec(event)?;
|
||||
let write_txn = db.begin_write()?;
|
||||
{
|
||||
let mut table = write_txn.open_table(NYANPASU_TABLE)?;
|
||||
@@ -176,7 +169,7 @@ impl TaskStorage {
|
||||
if event_ids.is_empty() {
|
||||
table.remove(event_ids_key.as_bytes())?;
|
||||
} else {
|
||||
let event_ids = simd_json::to_vec(&event_ids)?;
|
||||
let event_ids = serde_json::to_vec(&event_ids)?;
|
||||
table.insert(event_ids_key.as_bytes(), event_ids.as_slice())?;
|
||||
}
|
||||
}
|
||||
@@ -211,7 +204,7 @@ impl TaskGuard for TaskManager {
|
||||
let key = key.value();
|
||||
let mut value = value.value().to_owned();
|
||||
if key.starts_with(b"task:id:") {
|
||||
let task = simd_json::from_slice::<Task>(value.as_mut_slice())?;
|
||||
let task = serde_json::from_slice::<Task>(value.as_mut_slice())?;
|
||||
debug!(
|
||||
"restore task: {:?} {:?}",
|
||||
str::from_utf8(key).unwrap(),
|
||||
@@ -234,7 +227,7 @@ impl TaskGuard for TaskManager {
|
||||
let mut table = write_txn.open_table(NYANPASU_TABLE)?;
|
||||
for task in tasks {
|
||||
let key = format!("task:id:{}", task.id);
|
||||
let value = simd_json::to_vec(&task)?;
|
||||
let value = serde_json::to_vec(&task)?;
|
||||
table.insert(key.as_bytes(), value.as_slice())?;
|
||||
}
|
||||
}
|
||||
|
@@ -31,7 +31,7 @@ pub enum Error {
|
||||
DatabaseCommitOperationFailed(#[from] redb::CommitError),
|
||||
|
||||
#[error("json parse failed: {0:?}")]
|
||||
JsonParseFailed(#[from] simd_json::Error),
|
||||
JsonParseFailed(#[from] serde_json::Error),
|
||||
|
||||
#[error("task issue failed: {message:?}")]
|
||||
InnerTask {
|
||||
|
@@ -182,11 +182,11 @@ impl Runner for JSRunner {
|
||||
let boa_runner = wrap_result!(BoaRunner::try_new(), take_logs(logs));
|
||||
wrap_result!(boa_runner.setup_console(logger), take_logs(logs));
|
||||
let config = wrap_result!(
|
||||
simd_json::serde::to_string(&mapping)
|
||||
serde_json::to_string(&mapping)
|
||||
.map_err(|e| { std::io::Error::new(std::io::ErrorKind::InvalidData, e) }),
|
||||
take_logs(logs)
|
||||
);
|
||||
let config = simd_json::to_string(&config).unwrap(); // escape the string
|
||||
let config = serde_json::to_string(&config).unwrap(); // escape the string
|
||||
let execute_module = format!(
|
||||
r#"import process from "./{hash}.mjs";
|
||||
let config = JSON.parse({config});
|
||||
@@ -220,7 +220,7 @@ impl Runner for JSRunner {
|
||||
take_logs(logs)
|
||||
);
|
||||
let mapping = wrap_result!(
|
||||
unsafe { simd_json::serde::from_str::<Mapping>(&mut result) }
|
||||
serde_json::from_str(&result)
|
||||
.map_err(|e| { std::io::Error::new(std::io::ErrorKind::InvalidData, e) }),
|
||||
take_logs(logs)
|
||||
);
|
||||
@@ -451,7 +451,7 @@ const foreignNameservers = [
|
||||
"Test".to_string()
|
||||
),])
|
||||
);
|
||||
let outs = simd_json::serde::to_string(&logs).unwrap();
|
||||
let outs = serde_json::to_string(&logs).unwrap();
|
||||
assert_eq!(
|
||||
outs,
|
||||
r#"[["log","Test console log"],["warn","Test console log"],["error","Test console log"]]"#
|
||||
@@ -516,7 +516,7 @@ const foreignNameservers = [
|
||||
serde_yaml::Value::String("RULE-SET,custom-proxy,🚀".to_string())
|
||||
])
|
||||
);
|
||||
let outs = simd_json::serde::to_string(&logs).unwrap();
|
||||
let outs = serde_json::to_string(&logs).unwrap();
|
||||
assert_eq!(outs, r#"[]"#);
|
||||
});
|
||||
}
|
||||
|
12
clash-nyanpasu/backend/tauri/src/event_handler/mod.rs
Normal file
12
clash-nyanpasu/backend/tauri/src/event_handler/mod.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
/// This module is a tauri event based handler.
|
||||
/// Some state is good to be managed by the Tauri Manager. we should not hold the singletons in the global state in some cases.
|
||||
use tauri::{Emitter, Listener, Manager, Runtime};
|
||||
|
||||
mod widget;
|
||||
|
||||
pub fn mount_handlers<M, R>(app: &mut M)
|
||||
where
|
||||
M: Manager<R> + Listener<R> + Emitter<R>,
|
||||
R: Runtime,
|
||||
{
|
||||
}
|
35
clash-nyanpasu/backend/tauri/src/event_handler/widget.rs
Normal file
35
clash-nyanpasu/backend/tauri/src/event_handler/widget.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use crate::config::nyanpasu::NetworkStatisticWidgetConfig;
|
||||
use anyhow::Context;
|
||||
use tauri::{AppHandle, Event, Runtime};
|
||||
|
||||
pub enum WidgetInstance {
|
||||
Small(nyanpasu_egui::widget::NyanpasuNetworkStatisticSmallWidget),
|
||||
Large(nyanpasu_egui::widget::NyanpasuNetworkStatisticLargeWidget),
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(app_handle))]
|
||||
pub(super) fn on_network_statistic_config_changed<R: Runtime>(
|
||||
app_handle: &AppHandle<R>,
|
||||
event: Event,
|
||||
) -> anyhow::Result<()> {
|
||||
// let config: NetworkStatisticWidgetConfig =
|
||||
// serde_json::from_str(event.payload()).context("failed to deserialize the new config")?;
|
||||
// match config {
|
||||
// NetworkStatisticWidgetConfig::Disabled => {
|
||||
// app_handle
|
||||
// .emit_all("network-statistic-widget:hide")
|
||||
// .context("failed to emit the hide event")?;
|
||||
// }
|
||||
// NetworkStatisticWidgetConfig::Large => {
|
||||
// app_handle
|
||||
// .emit_all("network-statistic-widget:show-large")
|
||||
// .context("failed to emit the show-large event")?;
|
||||
// }
|
||||
// NetworkStatisticWidgetConfig::Small => {
|
||||
// app_handle
|
||||
// .emit_all("network-statistic-widget:show-small")
|
||||
// .context("failed to emit the show-small event")?;
|
||||
// }
|
||||
// }
|
||||
Ok(())
|
||||
}
|
@@ -7,16 +7,17 @@
|
||||
use std::borrow::Borrow;
|
||||
|
||||
use crate::{
|
||||
config::*,
|
||||
config::{nyanpasu::NetworkStatisticWidgetConfig, *},
|
||||
core::{service::ipc::get_ipc_state, *},
|
||||
log_err,
|
||||
utils::{self, help::get_clash_external_port, resolve},
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
use handle::Message;
|
||||
use nyanpasu_egui::widget::network_statistic_large;
|
||||
use nyanpasu_ipc::api::status::CoreState;
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use tauri::AppHandle;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
|
||||
// 打开面板
|
||||
@@ -285,7 +286,7 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
||||
let log_level = patch.app_log_level;
|
||||
let log_max_files = patch.max_log_files;
|
||||
let enable_tray_selector = patch.clash_tray_selector;
|
||||
|
||||
let network_statistic_widget = patch.network_statistic_widget;
|
||||
let res = || async move {
|
||||
let service_mode = patch.enable_service_mode;
|
||||
let ipc_state = get_ipc_state();
|
||||
@@ -362,6 +363,24 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
||||
handle::Handle::update_systray()?;
|
||||
}
|
||||
|
||||
// TODO: refactor config with changed notify
|
||||
if network_statistic_widget.is_some() {
|
||||
let network_statistic_widget = network_statistic_widget.unwrap();
|
||||
let widget_manager =
|
||||
crate::consts::app_handle().state::<crate::widget::WidgetManager>();
|
||||
let is_running = widget_manager.is_running().await;
|
||||
match network_statistic_widget {
|
||||
NetworkStatisticWidgetConfig::Disabled => {
|
||||
if is_running {
|
||||
widget_manager.stop().await?;
|
||||
}
|
||||
}
|
||||
NetworkStatisticWidgetConfig::Enabled(variant) => {
|
||||
widget_manager.start(variant).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<Result<()>>::Ok(())
|
||||
};
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
config::*,
|
||||
config::{profile::ProfileBuilder, *},
|
||||
core::{storage::Storage, tasks::jobs::ProfilesJobGuard, updater::ManifestVersionLatest, *},
|
||||
enhance::PostProcessingOutput,
|
||||
feat,
|
||||
@@ -147,26 +147,26 @@ pub async fn import_profile(url: String, option: Option<RemoteProfileOptionsBuil
|
||||
/// create a new profile
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn create_profile(item: ProfileKind, file_data: Option<String>) -> Result {
|
||||
pub async fn create_profile(item: ProfileBuilder, file_data: Option<String>) -> Result {
|
||||
tracing::trace!("create profile: {item:?}");
|
||||
|
||||
let is_remote = matches!(&item, ProfileKind::Remote(_));
|
||||
let is_remote = matches!(&item, ProfileBuilder::Remote(_));
|
||||
|
||||
let profile: Profile = match item {
|
||||
ProfileKind::Local(builder) => builder
|
||||
ProfileBuilder::Local(builder) => builder
|
||||
.build()
|
||||
.context("failed to build local profile")?
|
||||
.into(),
|
||||
ProfileKind::Remote(mut builder) => builder
|
||||
ProfileBuilder::Remote(mut builder) => builder
|
||||
.build_no_blocking()
|
||||
.await
|
||||
.context("failed to build remote profile")?
|
||||
.into(),
|
||||
ProfileKind::Merge(builder) => builder
|
||||
ProfileBuilder::Merge(builder) => builder
|
||||
.build()
|
||||
.context("failed to build merge profile")?
|
||||
.into(),
|
||||
ProfileKind::Script(builder) => builder
|
||||
ProfileBuilder::Script(builder) => builder
|
||||
.build()
|
||||
.context("failed to build script profile")?
|
||||
.into(),
|
||||
@@ -277,7 +277,7 @@ pub async fn patch_profiles_config(profiles: ProfilesBuilder) -> Result {
|
||||
/// update profile by uid
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn patch_profile(app_handle: AppHandle, uid: String, profile: ProfileKind) -> Result {
|
||||
pub async fn patch_profile(app_handle: AppHandle, uid: String, profile: ProfileBuilder) -> Result {
|
||||
tracing::debug!("patch profile: {uid} with {profile:?}");
|
||||
{
|
||||
let committer = Config::profiles().auto_commit();
|
||||
@@ -419,9 +419,6 @@ pub fn get_postprocessing_output() -> Result<PostProcessingOutput> {
|
||||
Ok(Config::runtime().latest().postprocessing_output.clone())
|
||||
}
|
||||
|
||||
#[derive(specta::Type)]
|
||||
pub struct Test<'n>(Cow<'n, CoreState>, i64, RunType);
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_core_status<'n>() -> Result<(Cow<'n, CoreState>, i64, RunType)> {
|
||||
@@ -971,3 +968,12 @@ pub fn remove_storage_item(app_handle: AppHandle, key: String) -> Result {
|
||||
(storage.remove_item(&key))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_clash_ws_connections_state(
|
||||
app_handle: AppHandle,
|
||||
) -> Result<crate::core::clash::ws::ClashConnectionsConnectorState> {
|
||||
let ws_connector = app_handle.state::<crate::core::clash::ws::ClashConnectionsConnector>();
|
||||
Ok(ws_connector.state())
|
||||
}
|
||||
|
@@ -10,11 +10,13 @@ mod config;
|
||||
mod consts;
|
||||
mod core;
|
||||
mod enhance;
|
||||
mod event_handler;
|
||||
mod feat;
|
||||
mod ipc;
|
||||
mod server;
|
||||
mod setup;
|
||||
mod utils;
|
||||
mod widget;
|
||||
mod window;
|
||||
|
||||
use std::io;
|
||||
@@ -26,7 +28,7 @@ use crate::{
|
||||
};
|
||||
use specta_typescript::{BigIntExportBehavior, Typescript};
|
||||
use tauri::{Emitter, Manager};
|
||||
use tauri_specta::collect_commands;
|
||||
use tauri_specta::{collect_commands, collect_events};
|
||||
use utils::resolve::{is_window_opened, reset_window_open_counter};
|
||||
|
||||
rust_i18n::i18n!("../../locales");
|
||||
@@ -176,84 +178,88 @@ pub fn run() -> std::io::Result<()> {
|
||||
}));
|
||||
|
||||
// setup specta
|
||||
let specta_builder = tauri_specta::Builder::<tauri::Wry>::new().commands(collect_commands![
|
||||
// common
|
||||
ipc::get_sys_proxy,
|
||||
ipc::open_app_config_dir,
|
||||
ipc::open_app_data_dir,
|
||||
ipc::open_logs_dir,
|
||||
ipc::open_web_url,
|
||||
ipc::open_core_dir,
|
||||
// cmds::kill_sidecar,
|
||||
ipc::restart_sidecar,
|
||||
// clash
|
||||
ipc::get_clash_info,
|
||||
ipc::get_clash_logs,
|
||||
ipc::patch_clash_config,
|
||||
ipc::change_clash_core,
|
||||
ipc::get_runtime_config,
|
||||
ipc::get_runtime_yaml,
|
||||
ipc::get_runtime_exists,
|
||||
ipc::get_postprocessing_output,
|
||||
ipc::clash_api_get_proxy_delay,
|
||||
ipc::uwp::invoke_uwp_tool,
|
||||
// updater
|
||||
ipc::fetch_latest_core_versions,
|
||||
ipc::update_core,
|
||||
ipc::inspect_updater,
|
||||
ipc::get_core_version,
|
||||
// utils
|
||||
ipc::collect_logs,
|
||||
// verge
|
||||
ipc::get_verge_config,
|
||||
ipc::patch_verge_config,
|
||||
// cmds::update_hotkeys,
|
||||
// profile
|
||||
ipc::get_profiles,
|
||||
ipc::enhance_profiles,
|
||||
ipc::patch_profiles_config,
|
||||
ipc::view_profile,
|
||||
ipc::patch_profile,
|
||||
ipc::create_profile,
|
||||
ipc::import_profile,
|
||||
ipc::reorder_profile,
|
||||
ipc::reorder_profiles_by_list,
|
||||
ipc::update_profile,
|
||||
ipc::delete_profile,
|
||||
ipc::read_profile_file,
|
||||
ipc::save_profile_file,
|
||||
ipc::save_window_size_state,
|
||||
ipc::get_custom_app_dir,
|
||||
ipc::set_custom_app_dir,
|
||||
// service mode
|
||||
ipc::service::status_service,
|
||||
ipc::service::install_service,
|
||||
ipc::service::uninstall_service,
|
||||
ipc::service::start_service,
|
||||
ipc::service::stop_service,
|
||||
ipc::service::restart_service,
|
||||
ipc::is_portable,
|
||||
ipc::get_proxies,
|
||||
ipc::select_proxy,
|
||||
ipc::update_proxy_provider,
|
||||
ipc::restart_application,
|
||||
ipc::collect_envs,
|
||||
ipc::get_server_port,
|
||||
ipc::set_tray_icon,
|
||||
ipc::is_tray_icon_set,
|
||||
ipc::get_core_status,
|
||||
ipc::url_delay_test,
|
||||
ipc::get_ipsb_asn,
|
||||
ipc::open_that,
|
||||
ipc::is_appimage,
|
||||
ipc::get_service_install_prompt,
|
||||
ipc::cleanup_processes,
|
||||
ipc::get_storage_item,
|
||||
ipc::set_storage_item,
|
||||
ipc::remove_storage_item,
|
||||
ipc::mutate_proxies,
|
||||
ipc::get_core_dir,
|
||||
]);
|
||||
let specta_builder = tauri_specta::Builder::<tauri::Wry>::new()
|
||||
.commands(collect_commands![
|
||||
// common
|
||||
ipc::get_sys_proxy,
|
||||
ipc::open_app_config_dir,
|
||||
ipc::open_app_data_dir,
|
||||
ipc::open_logs_dir,
|
||||
ipc::open_web_url,
|
||||
ipc::open_core_dir,
|
||||
// cmds::kill_sidecar,
|
||||
ipc::restart_sidecar,
|
||||
// clash
|
||||
ipc::get_clash_info,
|
||||
ipc::get_clash_logs,
|
||||
ipc::patch_clash_config,
|
||||
ipc::change_clash_core,
|
||||
ipc::get_runtime_config,
|
||||
ipc::get_runtime_yaml,
|
||||
ipc::get_runtime_exists,
|
||||
ipc::get_postprocessing_output,
|
||||
ipc::clash_api_get_proxy_delay,
|
||||
ipc::uwp::invoke_uwp_tool,
|
||||
// updater
|
||||
ipc::fetch_latest_core_versions,
|
||||
ipc::update_core,
|
||||
ipc::inspect_updater,
|
||||
ipc::get_core_version,
|
||||
// utils
|
||||
ipc::collect_logs,
|
||||
// verge
|
||||
ipc::get_verge_config,
|
||||
ipc::patch_verge_config,
|
||||
// cmds::update_hotkeys,
|
||||
// profile
|
||||
ipc::get_profiles,
|
||||
ipc::enhance_profiles,
|
||||
ipc::patch_profiles_config,
|
||||
ipc::view_profile,
|
||||
ipc::patch_profile,
|
||||
ipc::create_profile,
|
||||
ipc::import_profile,
|
||||
ipc::reorder_profile,
|
||||
ipc::reorder_profiles_by_list,
|
||||
ipc::update_profile,
|
||||
ipc::delete_profile,
|
||||
ipc::read_profile_file,
|
||||
ipc::save_profile_file,
|
||||
ipc::save_window_size_state,
|
||||
ipc::get_custom_app_dir,
|
||||
ipc::set_custom_app_dir,
|
||||
// service mode
|
||||
ipc::service::status_service,
|
||||
ipc::service::install_service,
|
||||
ipc::service::uninstall_service,
|
||||
ipc::service::start_service,
|
||||
ipc::service::stop_service,
|
||||
ipc::service::restart_service,
|
||||
ipc::is_portable,
|
||||
ipc::get_proxies,
|
||||
ipc::select_proxy,
|
||||
ipc::update_proxy_provider,
|
||||
ipc::restart_application,
|
||||
ipc::collect_envs,
|
||||
ipc::get_server_port,
|
||||
ipc::set_tray_icon,
|
||||
ipc::is_tray_icon_set,
|
||||
ipc::get_core_status,
|
||||
ipc::url_delay_test,
|
||||
ipc::get_ipsb_asn,
|
||||
ipc::open_that,
|
||||
ipc::is_appimage,
|
||||
ipc::get_service_install_prompt,
|
||||
ipc::cleanup_processes,
|
||||
ipc::get_storage_item,
|
||||
ipc::set_storage_item,
|
||||
ipc::remove_storage_item,
|
||||
ipc::mutate_proxies,
|
||||
ipc::get_core_dir,
|
||||
// clash layer
|
||||
ipc::get_clash_ws_connections_state,
|
||||
])
|
||||
.events(collect_events![core::clash::ClashConnectionsEvent]);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
@@ -310,7 +316,9 @@ pub fn run() -> std::io::Result<()> {
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::default().build())
|
||||
.setup(|app| {
|
||||
.setup(move |app| {
|
||||
specta_builder.mount_events(app);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use tauri::menu::{MenuBuilder, SubmenuBuilder};
|
||||
|
@@ -108,6 +108,7 @@ pub fn resolve_setup(app: &mut App) {
|
||||
});
|
||||
|
||||
handle::Handle::global().init(app.app_handle().clone());
|
||||
crate::consts::setup_app_handle(app.app_handle().clone());
|
||||
|
||||
log_err!(init::init_resources());
|
||||
log_err!(init::init_service());
|
||||
@@ -150,6 +151,19 @@ pub fn resolve_setup(app: &mut App) {
|
||||
log::trace!("launch core");
|
||||
log_err!(CoreManager::global().init());
|
||||
|
||||
log::trace!("init clash connection connector");
|
||||
log_err!(crate::core::clash::setup(app));
|
||||
|
||||
log::trace!("init widget manager");
|
||||
log_err!(tauri::async_runtime::block_on(async {
|
||||
crate::widget::setup(app, {
|
||||
let manager = app.state::<crate::core::clash::ws::ClashConnectionsConnector>();
|
||||
manager.subscribe()
|
||||
})
|
||||
.await
|
||||
}));
|
||||
|
||||
#[cfg(any(windows, target_os = "linux"))]
|
||||
log::trace!("init system tray");
|
||||
#[cfg(any(windows, target_os = "linux"))]
|
||||
tray::icon::resize_images(crate::utils::help::get_max_scale_factor()); // generate latest cache icon by current scale factor
|
||||
|
211
clash-nyanpasu/backend/tauri/src/widget.rs
Normal file
211
clash-nyanpasu/backend/tauri/src/widget.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
use crate::config::{nyanpasu::NetworkStatisticWidgetConfig, Config};
|
||||
|
||||
use super::core::clash::ws::ClashConnectionsConnectorEvent;
|
||||
|
||||
use anyhow::Context;
|
||||
use nyanpasu_egui::{
|
||||
ipc::{create_ipc_server, IpcSender, Message, StatisticMessage},
|
||||
widget::StatisticWidgetVariant,
|
||||
};
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use tauri::{utils::platform::current_exe, Manager, Runtime};
|
||||
use tokio::{
|
||||
process::Child,
|
||||
sync::{
|
||||
broadcast::{error::RecvError as BroadcastRecvError, Receiver as BroadcastReceiver},
|
||||
Mutex,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WidgetManager {
|
||||
instance: Arc<Mutex<Option<WidgetManagerInstance>>>,
|
||||
listener_initd: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
struct WidgetManagerInstance {
|
||||
tx: IpcSender<Message>,
|
||||
process: Child,
|
||||
}
|
||||
|
||||
impl WidgetManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
instance: Arc::new(Mutex::new(None)),
|
||||
listener_initd: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
fn register_listener(&self, mut receiver: BroadcastReceiver<ClashConnectionsConnectorEvent>) {
|
||||
if self
|
||||
.listener_initd
|
||||
.load(std::sync::atomic::Ordering::Acquire)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let signal = self.listener_initd.clone();
|
||||
let this = self.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match receiver.recv().await {
|
||||
Ok(event) => {
|
||||
if let Err(e) = this.handle_event(event).await {
|
||||
log::error!("Failed to handle event: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error receiving event: {}", e);
|
||||
if BroadcastRecvError::Closed == e {
|
||||
signal.store(false, std::sync::atomic::Ordering::Release);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
self.listener_initd
|
||||
.store(true, std::sync::atomic::Ordering::Release);
|
||||
}
|
||||
|
||||
async fn handle_event(&self, event: ClashConnectionsConnectorEvent) -> anyhow::Result<()> {
|
||||
let mut instance = self.instance.clone().lock_owned().await;
|
||||
if let ClashConnectionsConnectorEvent::Update(info) = event {
|
||||
if instance
|
||||
.as_mut()
|
||||
.is_some_and(|instance| instance.is_alive())
|
||||
{
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let instance = instance.as_ref().unwrap();
|
||||
// we only care about the update event now
|
||||
instance
|
||||
.tx
|
||||
.send(Message::UpdateStatistic(StatisticMessage {
|
||||
download_total: info.download_total,
|
||||
upload_total: info.upload_total,
|
||||
download_speed: info.download_speed,
|
||||
upload_speed: info.upload_speed,
|
||||
}))
|
||||
.context("Failed to send event to widget")?;
|
||||
Ok::<(), anyhow::Error>(())
|
||||
})
|
||||
.await
|
||||
.context("Failed to send event to widget")??;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn start(&self, widget: StatisticWidgetVariant) -> anyhow::Result<()> {
|
||||
let mut instance = self.instance.lock().await;
|
||||
if instance.is_some() {
|
||||
log::info!("Widget already running, stopping it first...");
|
||||
self.stop().await.context("Failed to stop widget")?;
|
||||
}
|
||||
let current_exe = current_exe().context("Failed to get current executable")?;
|
||||
// This operation is blocking, but it internal just a system call, so I think it's okay
|
||||
let (mut ipc_server, server_name) = create_ipc_server()?;
|
||||
// spawn a process to run the widget
|
||||
let child = tokio::process::Command::new(current_exe)
|
||||
.arg("statistic-widget")
|
||||
.arg(serde_json::to_string(&widget).context("Failed to serialize widget")?)
|
||||
.env("NYANPASU_EGUI_IPC_SERVER", server_name)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.context("Failed to spawn widget process")?;
|
||||
let tx = tokio::task::spawn_blocking(move || {
|
||||
ipc_server
|
||||
.connect()
|
||||
.context("Failed to connect to widget")?;
|
||||
ipc_server.into_tx().context("Failed to get ipc sender")
|
||||
})
|
||||
.await
|
||||
.context("Failed to read widget output")??;
|
||||
instance.replace(WidgetManagerInstance { tx, process: child });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn stop(&self) -> anyhow::Result<()> {
|
||||
let Some(mut instance) = self.instance.lock().await.take() else {
|
||||
return Ok(());
|
||||
};
|
||||
if !instance.is_alive() {
|
||||
return Ok(());
|
||||
}
|
||||
// first try to stop the process gracefully
|
||||
let mut instance = tokio::task::spawn_blocking(move || {
|
||||
instance
|
||||
.tx
|
||||
.send(Message::Stop)
|
||||
.context("Failed to send stop message to widget")?;
|
||||
Ok::<WidgetManagerInstance, anyhow::Error>(instance)
|
||||
})
|
||||
.await
|
||||
.context("Failed to kill widget process")??;
|
||||
for _ in 0..5 {
|
||||
if instance.is_alive() {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
// force kill the process
|
||||
instance
|
||||
.process
|
||||
.kill()
|
||||
.await
|
||||
.context("Failed to kill widget process")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn is_running(&self) -> bool {
|
||||
let mut instance = self.instance.lock().await;
|
||||
instance
|
||||
.as_mut()
|
||||
.is_some_and(|instance| instance.is_alive())
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetManagerInstance {
|
||||
pub fn is_alive(&mut self) -> bool {
|
||||
self.process.try_wait().is_ok_and(|status| status.is_none())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WidgetManager {
|
||||
fn drop(&mut self) {
|
||||
let cleanup = async {
|
||||
let _ = self.stop().await;
|
||||
};
|
||||
match tokio::runtime::Handle::try_current() {
|
||||
Ok(_) => {
|
||||
tokio::task::block_in_place(move || {
|
||||
tauri::async_runtime::block_on(cleanup);
|
||||
});
|
||||
}
|
||||
Err(_) => {
|
||||
tauri::async_runtime::block_on(cleanup);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn setup<R: Runtime, M: Manager<R>>(
|
||||
manager: &M,
|
||||
ws_connections_receiver: BroadcastReceiver<ClashConnectionsConnectorEvent>,
|
||||
) -> anyhow::Result<()> {
|
||||
let widget_manager = WidgetManager::new();
|
||||
// TODO: use the app_handle to read initial config.
|
||||
let option = Config::verge()
|
||||
.data()
|
||||
.network_statistic_widget
|
||||
.unwrap_or_default();
|
||||
widget_manager.register_listener(ws_connections_receiver);
|
||||
if let NetworkStatisticWidgetConfig::Enabled(widget) = option {
|
||||
widget_manager.start(widget).await?;
|
||||
}
|
||||
|
||||
// TODO: subscribe to the config change event
|
||||
manager.manage(widget_manager);
|
||||
Ok(())
|
||||
}
|
@@ -11,6 +11,7 @@
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "5.66.0",
|
||||
"@tauri-apps/api": "2.2.0",
|
||||
"ahooks": "3.8.4",
|
||||
"ofetch": "1.4.1",
|
||||
|
@@ -1,3 +1,5 @@
|
||||
export * from './ipc'
|
||||
export * from './service'
|
||||
export * from './openapi'
|
||||
export * from './provider'
|
||||
export * from './service'
|
||||
export * from './utils'
|
||||
|
@@ -304,7 +304,7 @@ export const commands = {
|
||||
*/
|
||||
async patchProfile(
|
||||
uid: string,
|
||||
profile: ProfileKind,
|
||||
profile: ProfileBuilder,
|
||||
): Promise<Result<null, string>> {
|
||||
try {
|
||||
return {
|
||||
@@ -320,7 +320,7 @@ export const commands = {
|
||||
* create a new profile
|
||||
*/
|
||||
async createProfile(
|
||||
item: ProfileKind,
|
||||
item: ProfileBuilder,
|
||||
fileData: string | null,
|
||||
): Promise<Result<null, string>> {
|
||||
try {
|
||||
@@ -707,10 +707,29 @@ export const commands = {
|
||||
else return { status: 'error', error: e as any }
|
||||
}
|
||||
},
|
||||
async getClashWsConnectionsState(): Promise<
|
||||
Result<ClashConnectionsConnectorState, string>
|
||||
> {
|
||||
try {
|
||||
return {
|
||||
status: 'ok',
|
||||
data: await TAURI_INVOKE('get_clash_ws_connections_state'),
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) throw e
|
||||
else return { status: 'error', error: e as any }
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/** user-defined events **/
|
||||
|
||||
export const events = __makeEvents__<{
|
||||
clashConnectionsEvent: ClashConnectionsEvent
|
||||
}>({
|
||||
clashConnectionsEvent: 'clash-connections-event',
|
||||
})
|
||||
|
||||
/** user-defined constants **/
|
||||
|
||||
/** user-defined types **/
|
||||
@@ -736,6 +755,20 @@ export type ChunkStatus = {
|
||||
speed: number
|
||||
}
|
||||
export type ChunkThreadState = 'Idle' | 'Downloading' | 'Finished'
|
||||
export type ClashConnectionsConnectorEvent =
|
||||
| { kind: 'state_changed'; data: ClashConnectionsConnectorState }
|
||||
| { kind: 'update'; data: ClashConnectionsInfo }
|
||||
export type ClashConnectionsConnectorState =
|
||||
| 'disconnected'
|
||||
| 'connecting'
|
||||
| 'connected'
|
||||
export type ClashConnectionsEvent = ClashConnectionsConnectorEvent
|
||||
export type ClashConnectionsInfo = {
|
||||
downloadTotal: number
|
||||
uploadTotal: number
|
||||
downloadSpeed: number
|
||||
uploadSpeed: number
|
||||
}
|
||||
export type ClashCore =
|
||||
| 'clash'
|
||||
| 'clash-rs'
|
||||
@@ -964,6 +997,10 @@ export type IVerge = {
|
||||
* TODO: 弃用此字段,转移到 clash config 里
|
||||
*/
|
||||
tun_stack: TunStack | null
|
||||
/**
|
||||
* 是否启用网络统计信息浮窗
|
||||
*/
|
||||
network_statistic_widget?: NetworkStatisticWidgetConfig | null
|
||||
}
|
||||
export type IVergeTheme = {
|
||||
primary_color: string | null
|
||||
@@ -1122,6 +1159,9 @@ export type MergeProfileBuilder = {
|
||||
*/
|
||||
updated: number | null
|
||||
}
|
||||
export type NetworkStatisticWidgetConfig =
|
||||
| { kind: 'disabled' }
|
||||
| { kind: 'enabled'; value: StatisticWidgetVariant }
|
||||
export type PatchRuntimeConfig = {
|
||||
allow_lan?: boolean | null
|
||||
ipv6?: boolean | null
|
||||
@@ -1148,20 +1188,20 @@ export type PostProcessingOutput = {
|
||||
advice: [LogSpan, string][]
|
||||
}
|
||||
export type Profile =
|
||||
| { Remote: RemoteProfile }
|
||||
| { Local: LocalProfile }
|
||||
| { Merge: MergeProfile }
|
||||
| { Script: ScriptProfile }
|
||||
| RemoteProfile
|
||||
| LocalProfile
|
||||
| MergeProfile
|
||||
| ScriptProfile
|
||||
export type ProfileBuilder =
|
||||
| RemoteProfileBuilder
|
||||
| LocalProfileBuilder
|
||||
| MergeProfileBuilder
|
||||
| ScriptProfileBuilder
|
||||
export type ProfileItemType =
|
||||
| 'remote'
|
||||
| 'local'
|
||||
| { script: ScriptType }
|
||||
| 'merge'
|
||||
export type ProfileKind =
|
||||
| { Remote: RemoteProfileBuilder }
|
||||
| { Local: LocalProfileBuilder }
|
||||
| { Merge: MergeProfileBuilder }
|
||||
| { Script: ScriptProfileBuilder }
|
||||
/**
|
||||
* Define the `profiles.yaml` schema
|
||||
*/
|
||||
@@ -1443,6 +1483,7 @@ export type ScriptProfileBuilder = {
|
||||
}
|
||||
export type ScriptType = 'javascript' | 'lua'
|
||||
export type ServiceStatus = 'not_installed' | 'stopped' | 'running'
|
||||
export type StatisticWidgetVariant = 'large' | 'small'
|
||||
export type StatusInfo = {
|
||||
name: string
|
||||
version: string
|
||||
|
@@ -0,0 +1,80 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { unwrapResult } from '../utils'
|
||||
import { commands } from './bindings'
|
||||
|
||||
/**
|
||||
* A custom hook that manages profile content data fetching and updating.
|
||||
*
|
||||
* @remarks
|
||||
* This hook provides functionality to read and write profile content using React Query.
|
||||
* It includes both query and mutation capabilities for profile data management.
|
||||
*
|
||||
* @param uid - The unique identifier for the profile
|
||||
*
|
||||
* @returns An object containing:
|
||||
* - `query` - The React Query result object for fetching profile content
|
||||
* - `upsert` - Mutation object for saving/updating profile content
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { query, upsert } = useProfileContent("user123");
|
||||
* const { data, isLoading } = query;
|
||||
*
|
||||
* // To update profile content
|
||||
* upsert.mutate("new profile content");
|
||||
* ```
|
||||
*/
|
||||
export const useProfileContent = (uid: string) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
/**
|
||||
* A React Query hook that fetches profile content based on a user ID.
|
||||
*
|
||||
* @remarks
|
||||
* This query uses the `readProfileFile` command to retrieve profile data
|
||||
* and unwraps the result.
|
||||
*
|
||||
* @param uid - The user ID used to fetch the profile content
|
||||
* @returns A React Query result object containing the profile content data,
|
||||
* loading state, and error state
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { data, isLoading } = useQuery(['profileContent', userId]);
|
||||
* ```
|
||||
*/
|
||||
const query = useQuery({
|
||||
queryKey: ['profileContent', uid],
|
||||
queryFn: async () => {
|
||||
return unwrapResult(await commands.readProfileFile(uid))
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Mutation hook for saving and updating profile file data
|
||||
*
|
||||
* @remarks
|
||||
* This mutation will invalidate the profile content query cache on success
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const { mutate } = upsert;
|
||||
* mutate("profile content");
|
||||
* ```
|
||||
*
|
||||
* @returns A mutation object that handles saving profile file data
|
||||
*/
|
||||
const upsert = useMutation({
|
||||
mutationFn: async (fileData: string) => {
|
||||
return unwrapResult(await commands.saveProfileFile(uid, fileData))
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['profileContent', uid] })
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
query,
|
||||
upsert,
|
||||
}
|
||||
}
|
227
clash-nyanpasu/frontend/interface/src/ipc/use-profile.ts
Normal file
227
clash-nyanpasu/frontend/interface/src/ipc/use-profile.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { unwrapResult } from '../utils'
|
||||
import { commands, ProfileBuilder, ProfilesBuilder } from './bindings'
|
||||
|
||||
type URLImportParams = Parameters<typeof commands.importProfile>
|
||||
|
||||
type ManualImportParams = Parameters<typeof commands.createProfile>
|
||||
|
||||
type CreateParams =
|
||||
| {
|
||||
type: 'url'
|
||||
data: {
|
||||
url: URLImportParams[0]
|
||||
option: URLImportParams[1]
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: 'manual'
|
||||
data: {
|
||||
item: ManualImportParams[0]
|
||||
fileData: ManualImportParams[1]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom hook for managing profile operations using React Query.
|
||||
* Provides functionality for CRUD operations on profiles including creation,
|
||||
* updating, reordering, and deletion.
|
||||
*
|
||||
* @returns An object containing:
|
||||
* - query: {@link UseQueryResult} Hook result for fetching profiles data
|
||||
* - create: {@link UseMutationResult} Mutation for creating/importing profiles
|
||||
* - update: {@link UseMutationResult} Mutation for updating existing profiles
|
||||
* - sort: {@link UseMutationResult} Mutation for reordering profiles
|
||||
* - upsert: {@link UseMutationResult} Mutation for upserting profile configurations
|
||||
* - drop: {@link UseMutationResult} Mutation for deleting profiles
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const { query, create, update, sort, upsert, drop } = useProfile();
|
||||
*
|
||||
* // Fetch profiles
|
||||
* const { data, isLoading } = query;
|
||||
*
|
||||
* // Create a new profile
|
||||
* create.mutate({
|
||||
* type: 'file',
|
||||
* data: { item: profileData, fileData: 'config' }
|
||||
* });
|
||||
*
|
||||
* // Update a profile
|
||||
* update.mutate({ uid: 'profile-id', profile: updatedProfile });
|
||||
*
|
||||
* // Reorder profiles
|
||||
* sort.mutate(['uid1', 'uid2', 'uid3']);
|
||||
*
|
||||
* // Upsert profile config
|
||||
* upsert.mutate(profilesConfig);
|
||||
*
|
||||
* // Delete a profile
|
||||
* drop.mutate('profile-id');
|
||||
* ```
|
||||
*/
|
||||
export const useProfile = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
/**
|
||||
* A React Query hook that fetches profiles data.
|
||||
* data is the full Profile configuration, including current, chain, valid, and items fields
|
||||
* Uses the `getProfiles` command to retrieve profile information.
|
||||
*
|
||||
* @returns {UseQueryResult} A query result object containing:
|
||||
* - data: {
|
||||
* current: string | null - Currently selected profile UID
|
||||
* chain: string[] - Global chain of profile UIDs
|
||||
* valid: boolean - Whether the profile configuration is valid
|
||||
* items: Profile[] - Array of profile configurations
|
||||
* }
|
||||
* - `isLoading`: Boolean indicating if the query is in loading state
|
||||
* - `error`: Error object if the query failed
|
||||
* - Other standard React Query result properties
|
||||
*/
|
||||
const query = useQuery({
|
||||
queryKey: ['profiles'],
|
||||
queryFn: async () => {
|
||||
return unwrapResult(await commands.getProfiles())
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Mutation hook for creating or importing profiles
|
||||
*
|
||||
* @remarks
|
||||
* This mutation handles two types of profile creation:
|
||||
* 1. URL-based import using `importProfile` command
|
||||
* 2. Direct creation using `createProfile` command
|
||||
*
|
||||
* @returns A mutation object that accepts CreateParams and handles profile creation
|
||||
*
|
||||
* @throws Will throw an error if the profile creation/import fails
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const { mutate } = create();
|
||||
* // Import from URL
|
||||
* mutate({ type: 'url', data: { url: 'https://example.com/config.yaml', option: {...} }});
|
||||
* // Create directly
|
||||
* mutate({ type: 'file', data: { item: {...}, fileData: '...' }});
|
||||
* ```
|
||||
*/
|
||||
const create = useMutation({
|
||||
mutationFn: async ({ type, data }: CreateParams) => {
|
||||
if (type === 'url') {
|
||||
const { url, option } = data
|
||||
return unwrapResult(await commands.importProfile(url, option))
|
||||
} else {
|
||||
const { item, fileData } = data
|
||||
return unwrapResult(await commands.createProfile(item, fileData))
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate and refetch
|
||||
queryClient.invalidateQueries({ queryKey: ['profiles'] })
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Mutation hook for updating a profile.
|
||||
* Uses React Query's useMutation to handle profile updates.
|
||||
*
|
||||
* @remarks
|
||||
* This mutation will automatically invalidate and refetch the 'profiles' query on success
|
||||
*
|
||||
* @param uid - The unique identifier of the profile to update
|
||||
* @param profile - The profile data of type ProfileBuilder to update with
|
||||
*
|
||||
* @returns A mutation object containing mutate function and mutation state
|
||||
*
|
||||
* @throws Will throw an error if the profile update fails
|
||||
*/
|
||||
const update = useMutation({
|
||||
mutationFn: async ({
|
||||
uid,
|
||||
profile,
|
||||
}: {
|
||||
uid: string
|
||||
profile: ProfileBuilder
|
||||
}) => {
|
||||
return unwrapResult(await commands.patchProfile(uid, profile))
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate and refetch
|
||||
queryClient.invalidateQueries({ queryKey: ['profiles'] })
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Mutation hook for reordering profiles.
|
||||
* Uses the React Query's useMutation hook to handle profile reordering operations.
|
||||
*
|
||||
* @remarks
|
||||
* This mutation takes an array of profile UIDs and reorders them according to the new sequence.
|
||||
* On successful reordering, it invalidates the 'profiles' query cache to trigger a refresh.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const { mutate } = sort;
|
||||
* mutate(['uid1', 'uid2', 'uid3']);
|
||||
* ```
|
||||
*/
|
||||
const sort = useMutation({
|
||||
mutationFn: async (uids: string[]) => {
|
||||
return unwrapResult(await commands.reorderProfilesByList(uids))
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate and refetch
|
||||
queryClient.invalidateQueries({ queryKey: ['profiles'] })
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Mutation hook for upserting profile configurations.
|
||||
*
|
||||
* @remarks
|
||||
* This mutation handles the update/insert of profile configurations and invalidates
|
||||
* the profiles query cache on success.
|
||||
*
|
||||
* @returns A mutation object that:
|
||||
* - Accepts a ProfilesBuilder parameter for the mutation
|
||||
* - Returns the unwrapped result from patchProfilesConfig command
|
||||
* - Automatically invalidates the 'profiles' query cache on successful mutation
|
||||
*/
|
||||
const upsert = useMutation({
|
||||
mutationFn: async (options: ProfilesBuilder) => {
|
||||
return unwrapResult(await commands.patchProfilesConfig(options))
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['profiles'] })
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* A mutation hook for deleting a profile.
|
||||
*
|
||||
* @returns {UseMutationResult} A mutation object that:
|
||||
* - Accepts a profile UID as parameter
|
||||
* - Deletes the profile via commands.deleteProfile
|
||||
* - Automatically invalidates 'profiles' queries on success
|
||||
*/
|
||||
const drop = useMutation({
|
||||
mutationFn: async (uid: string) => {
|
||||
return unwrapResult(await commands.deleteProfile(uid))
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['profiles'] })
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
query,
|
||||
create,
|
||||
update,
|
||||
sort,
|
||||
upsert,
|
||||
drop,
|
||||
}
|
||||
}
|
10
clash-nyanpasu/frontend/interface/src/provider/index.tsx
Normal file
10
clash-nyanpasu/frontend/interface/src/provider/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
export const NyanpasuProvider = ({ children }: PropsWithChildren) => {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
)
|
||||
}
|
8
clash-nyanpasu/frontend/interface/src/utils/index.ts
Normal file
8
clash-nyanpasu/frontend/interface/src/utils/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { Result } from '../ipc/bindings'
|
||||
|
||||
export function unwrapResult<T, E>(res: Result<T, E>) {
|
||||
if (res.status === 'error') {
|
||||
throw res.error
|
||||
}
|
||||
return res.status === 'ok' ? res.data : undefined
|
||||
}
|
@@ -30,7 +30,7 @@
|
||||
"dayjs": "1.11.13",
|
||||
"framer-motion": "12.0.6",
|
||||
"i18next": "24.2.2",
|
||||
"jotai": "2.11.1",
|
||||
"jotai": "2.11.3",
|
||||
"json-schema": "0.4.0",
|
||||
"material-react-table": "3.1.0",
|
||||
"monaco-editor": "0.52.2",
|
||||
@@ -45,19 +45,20 @@
|
||||
"react-split-grid": "1.0.4",
|
||||
"react-use": "17.6.0",
|
||||
"swr": "2.3.0",
|
||||
"virtua": "0.39.3",
|
||||
"virtua": "0.40.0",
|
||||
"vite-bundle-visualizer": "1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@csstools/normalize.css": "12.1.1",
|
||||
"@emotion/babel-plugin": "11.13.5",
|
||||
"@emotion/react": "11.14.0",
|
||||
"@iconify/json": "2.2.301",
|
||||
"@iconify/json": "2.2.302",
|
||||
"@monaco-editor/react": "4.6.0",
|
||||
"@tanstack/react-query": "5.64.2",
|
||||
"@tanstack/react-router": "1.97.17",
|
||||
"@tanstack/router-devtools": "1.97.17",
|
||||
"@tanstack/router-plugin": "1.97.17",
|
||||
"@tailwindcss/vite": "4.0.3",
|
||||
"@tanstack/react-query": "5.66.0",
|
||||
"@tanstack/react-router": "1.99.0",
|
||||
"@tanstack/router-devtools": "1.99.0",
|
||||
"@tanstack/router-plugin": "1.99.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.2.1",
|
||||
"@tauri-apps/plugin-dialog": "2.2.0",
|
||||
"@tauri-apps/plugin-fs": "2.2.0",
|
||||
@@ -65,7 +66,7 @@
|
||||
"@tauri-apps/plugin-os": "2.2.0",
|
||||
"@tauri-apps/plugin-process": "2.2.0",
|
||||
"@tauri-apps/plugin-shell": "2.2.0",
|
||||
"@tauri-apps/plugin-updater": "2.3.1",
|
||||
"@tauri-apps/plugin-updater": "2.4.0",
|
||||
"@types/react": "19.0.8",
|
||||
"@types/react-dom": "19.0.3",
|
||||
"@types/validator": "13.12.2",
|
||||
@@ -80,7 +81,7 @@
|
||||
"monaco-yaml": "5.2.3",
|
||||
"nanoid": "5.0.9",
|
||||
"sass-embedded": "1.83.4",
|
||||
"shiki": "1.29.2",
|
||||
"shiki": "2.2.0",
|
||||
"tailwindcss-textshadow": "2.1.3",
|
||||
"unplugin-auto-import": "19.0.0",
|
||||
"unplugin-icons": "22.0.0",
|
||||
|
@@ -4,6 +4,5 @@ export default {
|
||||
'postcss-import': {},
|
||||
'postcss-html': {},
|
||||
autoprefixer: {},
|
||||
tailwindcss: {},
|
||||
},
|
||||
}
|
||||
|
@@ -1,3 +1,5 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
/* stylelint-disable import-notation */
|
||||
|
||||
@import 'tailwindcss';
|
||||
|
||||
@tailwind utilities;
|
||||
|
@@ -74,12 +74,12 @@ export const AppContainer = ({
|
||||
|
||||
<div className={styles.container}>
|
||||
{OS === 'windows' && (
|
||||
<LayoutControl className="!z-top fixed right-4 top-2" />
|
||||
<LayoutControl className="!z-top fixed top-2 right-4" />
|
||||
)}
|
||||
{/* TODO: add a framer motion animation to toggle the maximized state */}
|
||||
{OS === 'macos' && !isMaximized && (
|
||||
<div
|
||||
className="z-top fixed left-4 top-3 h-8 w-[4.5rem] rounded-full"
|
||||
className="z-top fixed top-3 left-4 h-8 w-[4.5rem] rounded-full"
|
||||
style={{ backgroundColor: alpha(palette.primary.main, 0.1) }}
|
||||
/>
|
||||
)}
|
||||
|
@@ -17,7 +17,7 @@ export const AppDrawer = () => {
|
||||
<div
|
||||
className={cn(
|
||||
'fixed z-10 flex items-center gap-2',
|
||||
OS === 'macos' ? 'left-24 top-3' : 'left-4 top-1.5',
|
||||
OS === 'macos' ? 'top-3 left-24' : 'top-1.5 left-4',
|
||||
)}
|
||||
data-tauri-drag-region
|
||||
>
|
||||
|
@@ -40,7 +40,7 @@ export const DrawerContent = ({
|
||||
|
||||
{!onlyIcon && (
|
||||
<div
|
||||
className="mt-1 flex-1 whitespace-pre-wrap text-lg font-bold"
|
||||
className="mt-1 flex-1 text-lg font-bold whitespace-pre-wrap"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
{'Clash\nNyanpasu'}
|
||||
@@ -48,7 +48,7 @@ export const DrawerContent = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="scrollbar-hidden flex flex-col gap-2 overflow-y-auto !overflow-x-hidden">
|
||||
<div className="scrollbar-hidden flex flex-col gap-2 !overflow-x-hidden overflow-y-auto">
|
||||
{Object.entries(routes).map(([name, { path, icon }]) => {
|
||||
return (
|
||||
<RouteListItem
|
||||
|
@@ -73,7 +73,7 @@ export const RouteListItem = ({
|
||||
{!onlyIcon && (
|
||||
<div
|
||||
className={cn(
|
||||
'w-full text-nowrap pb-1 pt-1',
|
||||
'w-full pt-1 pb-1 text-nowrap',
|
||||
nyanpasuConfig?.language &&
|
||||
languageQuirks[nyanpasuConfig?.language].drawer.itemClassNames,
|
||||
)}
|
||||
|
@@ -49,7 +49,7 @@ export const IPASNPanel = ({ refreshCount }: { refreshCount: number }) => {
|
||||
xl: 3,
|
||||
}}
|
||||
>
|
||||
<Paper className="relative flex !h-full select-text gap-4 !rounded-3xl px-4 py-3">
|
||||
<Paper className="relative flex !h-full gap-4 !rounded-3xl px-4 py-3 select-text">
|
||||
{data ? (
|
||||
<>
|
||||
{data.country_code && (
|
||||
@@ -102,7 +102,7 @@ export const IPASNPanel = ({ refreshCount }: { refreshCount: number }) => {
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
'absolute left-0 top-0 block h-full w-full rounded-lg bg-slate-300 transition-opacity',
|
||||
'absolute top-0 left-0 block h-full w-full rounded-lg bg-slate-300 transition-opacity',
|
||||
showIPAddress ? 'opacity-0' : 'animate-pulse opacity-100',
|
||||
)}
|
||||
/>
|
||||
@@ -120,7 +120,7 @@ export const IPASNPanel = ({ refreshCount }: { refreshCount: number }) => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-2 mt-1.5 h-9 w-12 animate-pulse rounded-lg bg-slate-700" />
|
||||
<div className="mt-1.5 mb-2 h-9 w-12 animate-pulse rounded-lg bg-slate-700" />
|
||||
|
||||
<div className="flex flex-1 animate-pulse flex-col gap-1">
|
||||
<div className="mt-1.5 h-6 w-20 rounded-full bg-slate-700" />
|
||||
|
@@ -22,7 +22,7 @@ export const LogItem = ({ value }: { value: LogMessage }) => {
|
||||
}, [value.payload])
|
||||
|
||||
return (
|
||||
<div className="w-full select-text p-4 pb-0 pt-2 font-mono">
|
||||
<div className="w-full p-4 pt-2 pb-0 font-mono select-text">
|
||||
<div className="flex gap-2">
|
||||
<span className="font-thin">{value.time}</span>
|
||||
|
||||
@@ -36,7 +36,7 @@ export const LogItem = ({ value }: { value: LogMessage }) => {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="text-wrap border-b border-slate-200 pb-2">
|
||||
<div className="border-b border-slate-200 pb-2 text-wrap">
|
||||
<p
|
||||
className={cn(
|
||||
styles.item,
|
||||
|
@@ -88,7 +88,7 @@ export const ChainItem = memo(function ChainItem({
|
||||
}}
|
||||
>
|
||||
<ListItemButton
|
||||
className="!mb-2 !mt-2 !flex !justify-between gap-2"
|
||||
className="!mt-2 !mb-2 !flex !justify-between gap-2"
|
||||
sx={[
|
||||
{
|
||||
borderRadius: 4,
|
||||
|
@@ -68,7 +68,7 @@ export const SideChain = ({ onChainEdit }: SideChainProps) => {
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto !pl-2 !pr-2">
|
||||
<div className="h-full overflow-auto !pr-2 !pl-2">
|
||||
<Reorder.Group
|
||||
axis="y"
|
||||
values={reorderValues}
|
||||
@@ -97,7 +97,7 @@ export const SideChain = ({ onChainEdit }: SideChainProps) => {
|
||||
</Reorder.Group>
|
||||
|
||||
<ListItemButton
|
||||
className="!mb-2 !mt-2 flex justify-center gap-2"
|
||||
className="!mt-2 !mb-2 flex justify-center gap-2"
|
||||
sx={{
|
||||
backgroundColor: alpha(palette.secondary.main, 0.1),
|
||||
borderRadius: 4,
|
||||
|
@@ -21,7 +21,7 @@ const LogListItem = memo(function LogListItem({
|
||||
<>
|
||||
{showDivider && <Divider />}
|
||||
|
||||
<div className="w-full break-all font-mono">
|
||||
<div className="w-full font-mono break-all">
|
||||
<span className="rounded-sm bg-blue-600 px-0.5">{name}</span>
|
||||
<span className="text-red-500"> [{item?.[0]}]: </span>
|
||||
<span>{item?.[1]}</span>
|
||||
@@ -53,7 +53,7 @@ export const SideLog = ({ className }: SideLogProps) => {
|
||||
|
||||
<Divider />
|
||||
|
||||
<VList className="flex select-text flex-col gap-2 overflow-auto p-2">
|
||||
<VList className="flex flex-col gap-2 overflow-auto p-2 select-text">
|
||||
{!isEmpty(getRuntimeLogs.data) ? (
|
||||
Object.entries(getRuntimeLogs.data).map(([uid, content]) => {
|
||||
return content.map((item, index) => {
|
||||
|
@@ -170,7 +170,7 @@ export const ProfileDialog = ({
|
||||
|
||||
const MetaInfo = useMemo(
|
||||
() => (
|
||||
<div className="flex flex-col gap-4 pb-2 pt-2">
|
||||
<div className="flex flex-col gap-4 pt-2 pb-2">
|
||||
{!isEdit && (
|
||||
<SelectElement
|
||||
label={t('Type')}
|
||||
|
@@ -234,7 +234,7 @@ export const ProfileItem = memo(function ProfileItem({
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Tooltip title={item.url}>
|
||||
<Chip
|
||||
className="!pl-2 !pr-2 font-bold"
|
||||
className="!pr-2 !pl-2 font-bold"
|
||||
avatar={<IconComponent className="!size-5" color="primary" />}
|
||||
label={isRemote ? t('Remote') : t('Local')}
|
||||
/>
|
||||
@@ -250,7 +250,7 @@ export const ProfileItem = memo(function ProfileItem({
|
||||
)}
|
||||
|
||||
<TextCarousel
|
||||
className="w-30 flex h-6 items-center"
|
||||
className="flex h-6 w-30 items-center"
|
||||
nodes={[
|
||||
!!item.updated && (
|
||||
<TimeSpan ts={item.updated!} k="Subscription Updated At" />
|
||||
@@ -348,7 +348,7 @@ export const ProfileItem = memo(function ProfileItem({
|
||||
|
||||
<motion.div
|
||||
className={cn(
|
||||
'absolute left-0 top-0 h-full w-full',
|
||||
'absolute top-0 left-0 h-full w-full',
|
||||
'flex-col items-center justify-center gap-4',
|
||||
'text-shadow-xl rounded-3xl font-bold backdrop-blur',
|
||||
)}
|
||||
@@ -379,7 +379,7 @@ function TimeSpan({ ts, k }: { ts: number; k: string }) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Tooltip title={time.format('YYYY/MM/DD HH:mm:ss')}>
|
||||
<div className="animate-marquee h-fit whitespace-nowrap text-right text-sm font-medium">
|
||||
<div className="animate-marquee h-fit text-right text-sm font-medium whitespace-nowrap">
|
||||
{t(k, {
|
||||
time: time.fromNow(),
|
||||
})}
|
||||
|
@@ -192,8 +192,8 @@ export const ScriptDialog = ({
|
||||
{...props}
|
||||
>
|
||||
<div className="flex h-full">
|
||||
<div className="overflow-auto pb-4 pt-4">
|
||||
<div className="flex flex-col gap-4 pb-4 pl-4 pr-4">
|
||||
<div className="overflow-auto pt-4 pb-4">
|
||||
<div className="flex flex-col gap-4 pr-4 pb-4 pl-4">
|
||||
{!isEdit && (
|
||||
<SelectElement
|
||||
label={t('Type')}
|
||||
|
@@ -47,7 +47,7 @@ export const DelayButton = memo(function DelayButton({
|
||||
return (
|
||||
<Tooltip title={t('Delay check')}>
|
||||
<Button
|
||||
className="!fixed bottom-8 right-8 z-10 size-16 !rounded-2xl backdrop-blur"
|
||||
className="!fixed right-8 bottom-8 z-10 size-16 !rounded-2xl backdrop-blur"
|
||||
sx={{
|
||||
boxShadow: 8,
|
||||
backgroundColor: alpha(
|
||||
|
@@ -37,7 +37,7 @@ const RuleItem = ({ index, value }: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex select-text p-2 pl-7 pr-7">
|
||||
<div className="flex p-2 pr-7 pl-7 select-text">
|
||||
<div style={{ color: palette.text.secondary }} className="min-w-14">
|
||||
{index + 1}
|
||||
</div>
|
||||
|
@@ -71,7 +71,7 @@ const CardProgress = ({
|
||||
return (
|
||||
<motion.div
|
||||
className={cn(
|
||||
'absolute left-0 top-0 z-10 h-full w-full rounded-2xl backdrop-blur',
|
||||
'absolute top-0 left-0 z-10 h-full w-full rounded-2xl backdrop-blur',
|
||||
'flex flex-col items-center justify-center gap-2',
|
||||
)}
|
||||
style={{
|
||||
|
@@ -44,7 +44,7 @@ export default function HotkeyInput({
|
||||
<div className={cn('relative min-h-[36px] w-[165px]', styles.wrapper)}>
|
||||
<input
|
||||
className={cn(
|
||||
'absolute left-0 top-0 z-[1] h-full w-full opacity-0',
|
||||
'absolute top-0 left-0 z-[1] h-full w-full opacity-0',
|
||||
styles.input,
|
||||
className,
|
||||
)}
|
||||
|
@@ -37,7 +37,7 @@ function CopyToClipboardButton({ onClick }: CopyToClipboardButtonProps) {
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
className="absolute right-1 top-1"
|
||||
className="absolute top-1 right-1"
|
||||
onClick={onClick}
|
||||
>
|
||||
<ContentPasteIcon fontSize="small" color="primary" />
|
||||
|
@@ -112,7 +112,7 @@ export const SettingNyanpasuVersion = () => {
|
||||
|
||||
{isPlatformSupported && (
|
||||
<>
|
||||
<div className="mb-1 mt-1">
|
||||
<div className="mt-1 mb-1">
|
||||
<AutoCheckUpdate />
|
||||
</div>
|
||||
<ListItem sx={{ pl: 0, pr: 0 }}>
|
||||
|
@@ -235,9 +235,9 @@ function ProfilePage() {
|
||||
</AnimatePresence>
|
||||
|
||||
<AddProfileContext.Provider value={addProfileCtxValue}>
|
||||
<div className="fixed bottom-8 right-8">
|
||||
<div className="fixed right-8 bottom-8">
|
||||
<FloatingButton
|
||||
className="relative -right-2.5 -top-3 flex size-11 min-w-fit"
|
||||
className="relative -top-3 -right-2.5 flex size-11 min-w-fit"
|
||||
sx={[
|
||||
(theme) => ({
|
||||
backgroundColor: theme.palette.grey[200],
|
||||
|
@@ -114,7 +114,7 @@ function ProxyPage() {
|
||||
onClick={() => handleSwitch(key)}
|
||||
sx={{ textTransform: 'capitalize' }}
|
||||
>
|
||||
{enabled && <Check className="-ml-2 mr-[0.1rem] scale-75" />}
|
||||
{enabled && <Check className="mr-[0.1rem] -ml-2 scale-75" />}
|
||||
{t(key)}
|
||||
</Button>
|
||||
))}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import type { Highlighter } from 'shiki'
|
||||
import { getSingletonHighlighterCore } from 'shiki/core'
|
||||
import { createOnigurumaEngine } from 'shiki/engine/oniguruma'
|
||||
import minLight from 'shiki/themes/min-light.mjs'
|
||||
import nord from 'shiki/themes/nord.mjs'
|
||||
import getWasm from 'shiki/wasm'
|
||||
@@ -9,6 +10,7 @@ let shiki: Highlighter | null = null
|
||||
export async function getShikiSingleton() {
|
||||
if (!shiki) {
|
||||
shiki = (await getSingletonHighlighterCore({
|
||||
engine: createOnigurumaEngine(import('shiki/wasm')),
|
||||
themes: [nord, minLight],
|
||||
langs: [() => import('shiki/langs/shell.mjs')],
|
||||
loadWasm: getWasm,
|
||||
|
@@ -8,6 +8,7 @@ import { createHtmlPlugin } from 'vite-plugin-html'
|
||||
import sassDts from 'vite-plugin-sass-dts'
|
||||
import svgr from 'vite-plugin-svgr'
|
||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||
import tailwindPlugin from '@tailwindcss/vite'
|
||||
// import react from "@vitejs/plugin-react";
|
||||
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
|
||||
import legacy from '@vitejs/plugin-legacy'
|
||||
@@ -56,6 +57,7 @@ export default defineConfig(({ command, mode }) => {
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
tailwindPlugin(),
|
||||
tsconfigPaths(),
|
||||
legacy({
|
||||
renderLegacyChunks: false,
|
||||
|
@@ -43,7 +43,7 @@
|
||||
"clsx": "2.1.1",
|
||||
"d3-interpolate-path": "2.3.0",
|
||||
"sass-embedded": "1.83.4",
|
||||
"tailwind-merge": "2.6.0",
|
||||
"tailwind-merge": "3.0.1",
|
||||
"typescript-plugin-css-modules": "5.1.0",
|
||||
"vite-plugin-dts": "4.5.0"
|
||||
}
|
||||
|
@@ -8,6 +8,6 @@
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
backdrop-filter: blur(4px);
|
||||
border-radius: 24px;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
@@ -125,7 +125,7 @@ export const BaseDialog = ({
|
||||
return (
|
||||
<AnimatePresence initial={false}>
|
||||
{mounted && (
|
||||
<Portal.Root className="fixed left-0 top-0 z-50 h-dvh w-full">
|
||||
<Portal.Root className="fixed top-0 left-0 z-50 h-dvh w-full">
|
||||
{!full && (
|
||||
<motion.div
|
||||
className={cn(
|
||||
@@ -150,7 +150,7 @@ export const BaseDialog = ({
|
||||
|
||||
<motion.div
|
||||
className={cn(
|
||||
'fixed left-[50%] top-[50%] z-50',
|
||||
'fixed top-[50%] left-[50%] z-50',
|
||||
full ? 'h-dvh w-full' : 'min-w-96 rounded-3xl shadow',
|
||||
palette.mode === 'dark'
|
||||
? 'text-white shadow-zinc-900'
|
||||
@@ -202,7 +202,7 @@ export const BaseDialog = ({
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'relative overflow-y-auto overflow-x-hidden p-4',
|
||||
'relative overflow-x-hidden overflow-y-auto p-4',
|
||||
full && 'h-full px-6',
|
||||
)}
|
||||
style={{
|
||||
|
@@ -9,7 +9,7 @@ export const Header: FC<{ title?: ReactNode; header?: ReactNode }> = memo(
|
||||
header?: ReactNode
|
||||
}) {
|
||||
return (
|
||||
<header className="select-none pl-2" data-tauri-drag-region>
|
||||
<header className="pl-2 select-none" data-tauri-drag-region>
|
||||
<h1 className="mb-1 text-4xl font-medium" data-tauri-drag-region>
|
||||
{title}
|
||||
</h1>
|
||||
|
@@ -50,7 +50,7 @@ export const BasePage: FC<BasePageProps> = ({
|
||||
</ScrollArea.Viewport>
|
||||
|
||||
<ScrollArea.Scrollbar
|
||||
className="flex touch-none select-none py-6 pr-1.5"
|
||||
className="flex touch-none py-6 pr-1.5 select-none"
|
||||
orientation="vertical"
|
||||
>
|
||||
<ScrollArea.Thumb className="ScrollArea-Thumb relative flex !w-1.5 flex-1 rounded-full" />
|
||||
|
@@ -17,7 +17,7 @@ export const FloatingButton = ({
|
||||
return (
|
||||
<Button
|
||||
className={cn(
|
||||
`bottom-8 right-8 z-10 size-16 !rounded-2xl backdrop-blur`,
|
||||
`right-8 bottom-8 z-10 size-16 !rounded-2xl backdrop-blur`,
|
||||
className,
|
||||
)}
|
||||
sx={{
|
||||
|
@@ -89,7 +89,7 @@ export const SidePage: FC<Props> = ({
|
||||
|
||||
<ScrollArea.Scrollbar
|
||||
className={cn(
|
||||
'flex touch-none select-none py-6 pr-1.5',
|
||||
'flex touch-none py-6 pr-1.5 select-none',
|
||||
sideBar && '!top-14',
|
||||
)}
|
||||
orientation="vertical"
|
||||
@@ -120,7 +120,7 @@ export const SidePage: FC<Props> = ({
|
||||
</ScrollArea.Viewport>
|
||||
|
||||
<ScrollArea.Scrollbar
|
||||
className="flex touch-none select-none py-6 pr-1.5"
|
||||
className="flex touch-none py-6 pr-1.5 select-none"
|
||||
orientation="vertical"
|
||||
>
|
||||
<ScrollArea.Thumb className="!bg-scroller relative flex !w-1.5 flex-1 rounded-full" />
|
||||
|
@@ -59,21 +59,21 @@
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "19.6.1",
|
||||
"@commitlint/config-conventional": "19.6.0",
|
||||
"@eslint/compat": "1.2.5",
|
||||
"@eslint/compat": "1.2.6",
|
||||
"@eslint/eslintrc": "3.2.0",
|
||||
"@ianvs/prettier-plugin-sort-imports": "4.4.1",
|
||||
"@tauri-apps/cli": "2.2.5",
|
||||
"@tauri-apps/cli": "2.2.7",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@types/node": "22.10.10",
|
||||
"@typescript-eslint/eslint-plugin": "8.21.0",
|
||||
"@typescript-eslint/parser": "8.21.0",
|
||||
"@types/node": "22.13.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.22.0",
|
||||
"@typescript-eslint/parser": "8.22.0",
|
||||
"autoprefixer": "10.4.20",
|
||||
"conventional-changelog-conventionalcommits": "8.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"dedent": "1.5.3",
|
||||
"eslint": "9.19.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-config-prettier": "10.0.1",
|
||||
"eslint-config-standard": "17.1.0",
|
||||
"eslint-import-resolver-alias": "1.1.2",
|
||||
"eslint-plugin-html": "8.1.2",
|
||||
@@ -86,7 +86,7 @@
|
||||
"eslint-plugin-react-hooks": "5.1.0",
|
||||
"globals": "15.14.0",
|
||||
"knip": "5.43.6",
|
||||
"lint-staged": "15.4.2",
|
||||
"lint-staged": "15.4.3",
|
||||
"neostandard": "0.12.0",
|
||||
"npm-run-all2": "7.0.2",
|
||||
"postcss": "8.5.1",
|
||||
@@ -96,18 +96,18 @@
|
||||
"prettier": "3.4.2",
|
||||
"prettier-plugin-tailwindcss": "0.6.11",
|
||||
"prettier-plugin-toml": "2.0.1",
|
||||
"react-devtools": "6.0.1",
|
||||
"stylelint": "16.13.2",
|
||||
"react-devtools": "6.1.0",
|
||||
"stylelint": "16.14.1",
|
||||
"stylelint-config-html": "1.1.0",
|
||||
"stylelint-config-recess-order": "5.1.1",
|
||||
"stylelint-config-standard": "36.0.1",
|
||||
"stylelint-config-recess-order": "6.0.0",
|
||||
"stylelint-config-standard": "37.0.0",
|
||||
"stylelint-declaration-block-no-ignored-properties": "2.8.0",
|
||||
"stylelint-order": "6.0.4",
|
||||
"stylelint-scss": "6.10.1",
|
||||
"tailwindcss": "3.4.17",
|
||||
"stylelint-scss": "6.11.0",
|
||||
"tailwindcss": "4.0.3",
|
||||
"tsx": "4.19.2",
|
||||
"typescript": "5.7.3",
|
||||
"typescript-eslint": "8.21.0"
|
||||
"typescript-eslint": "8.22.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.1.0",
|
||||
"engines": {
|
||||
|
1477
clash-nyanpasu/pnpm-lock.yaml
generated
1477
clash-nyanpasu/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@
|
||||
"p-retry": "6.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/types": "13.7.0",
|
||||
"@octokit/types": "13.8.0",
|
||||
"@types/adm-zip": "0.5.7",
|
||||
"adm-zip": "0.5.16",
|
||||
"colorize-template": "1.0.0",
|
||||
|
@@ -0,0 +1,57 @@
|
||||
From 4e432e530db0056450fbc4a3cee793f16adc39a7 Mon Sep 17 00:00:00 2001
|
||||
From: Daniel Golle <daniel@makrotopia.org>
|
||||
Date: Tue, 8 Oct 2024 23:58:41 +0100
|
||||
Subject: [PATCH] net: phy: populate host_interfaces when attaching PHY
|
||||
|
||||
Use bitmask of interfaces supported by the MAC for the PHY to choose
|
||||
from if the declared interface mode is among those using a single pair
|
||||
of SerDes lanes.
|
||||
This will allow 2500Base-T PHYs to switch to SGMII on most hosts, which
|
||||
results in half-duplex being supported in case the MAC supports them.
|
||||
Without this change, 2500Base-T PHYs will always operate in 2500Base-X
|
||||
mode with rate-matching, which is not only wasteful in terms of energy
|
||||
consumption, but also limits the supported interface modes to
|
||||
full-duplex only.
|
||||
|
||||
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
|
||||
---
|
||||
drivers/net/phy/phylink.c | 21 ++++++++++++++++++++-
|
||||
1 file changed, 20 insertions(+), 1 deletion(-)
|
||||
|
||||
--- a/drivers/net/phy/phylink.c
|
||||
+++ b/drivers/net/phy/phylink.c
|
||||
@@ -2017,7 +2017,7 @@ int phylink_fwnode_phy_connect(struct ph
|
||||
{
|
||||
struct fwnode_handle *phy_fwnode;
|
||||
struct phy_device *phy_dev;
|
||||
- int ret;
|
||||
+ int i, ret;
|
||||
|
||||
/* Fixed links and 802.3z are handled without needing a PHY */
|
||||
if (pl->cfg_link_an_mode == MLO_AN_FIXED ||
|
||||
@@ -2044,6 +2044,25 @@ int phylink_fwnode_phy_connect(struct ph
|
||||
pl->link_config.interface = pl->link_interface;
|
||||
}
|
||||
|
||||
+ /* Assume single-lane SerDes interface modes share the same
|
||||
+ * lanes and allow the PHY to switch to slower also supported modes
|
||||
+ */
|
||||
+ for (i = ARRAY_SIZE(phylink_sfp_interface_preference) - 1; i >= 0; i--) {
|
||||
+ /* skip unsupported modes */
|
||||
+ if (!test_bit(phylink_sfp_interface_preference[i], pl->config->supported_interfaces))
|
||||
+ continue;
|
||||
+
|
||||
+ __set_bit(phylink_sfp_interface_preference[i], phy_dev->host_interfaces);
|
||||
+
|
||||
+ /* skip all faster modes */
|
||||
+ if (phylink_sfp_interface_preference[i] == pl->link_interface)
|
||||
+ break;
|
||||
+ }
|
||||
+
|
||||
+ if (test_bit(pl->link_interface, phylink_sfp_interfaces))
|
||||
+ phy_interface_and(phy_dev->host_interfaces, phylink_sfp_interfaces,
|
||||
+ pl->config->supported_interfaces);
|
||||
+
|
||||
ret = phy_attach_direct(pl->netdev, phy_dev, flags,
|
||||
pl->link_interface);
|
||||
phy_device_free(phy_dev);
|
@@ -352,15 +352,15 @@ o:value("119.28.28.28")
|
||||
o:depends("direct_dns_mode", "tcp")
|
||||
|
||||
o = s:taboption("DNS", Value, "direct_dns_dot", translate("Direct DNS DoT"))
|
||||
o.default = "tls://dot.pub@1.12.12.12"
|
||||
o:value("tls://dot.pub@1.12.12.12")
|
||||
o:value("tls://dot.pub@120.53.53.53")
|
||||
o:value("tls://dot.360.cn@36.99.170.86")
|
||||
o:value("tls://dot.360.cn@101.198.191.4")
|
||||
o:value("tls://dns.alidns.com@223.5.5.5")
|
||||
o:value("tls://dns.alidns.com@223.6.6.6")
|
||||
o:value("tls://dns.alidns.com@2400:3200::1")
|
||||
o:value("tls://dns.alidns.com@2400:3200:baba::1")
|
||||
o.default = "tls://1.12.12.12"
|
||||
o:value("tls://1.12.12.12")
|
||||
o:value("tls://120.53.53.53")
|
||||
o:value("tls://36.99.170.86")
|
||||
o:value("tls://101.198.191.4")
|
||||
o:value("tls://223.5.5.5")
|
||||
o:value("tls://223.6.6.6")
|
||||
o:value("tls://2400:3200::1")
|
||||
o:value("tls://2400:3200:baba::1")
|
||||
o.validate = chinadns_dot_validate
|
||||
o:depends("direct_dns_mode", "dot")
|
||||
|
||||
@@ -502,17 +502,17 @@ o:depends({singbox_dns_mode = "tcp"})
|
||||
|
||||
---- DoT
|
||||
o = s:taboption("DNS", Value, "remote_dns_dot", translate("Remote DNS DoT"))
|
||||
o.default = "tls://dns.google@8.8.4.4"
|
||||
o:value("tls://1dot1dot1dot1.cloudflare-dns.com@1.0.0.1", "1.0.0.1 (CloudFlare)")
|
||||
o:value("tls://1dot1dot1dot1.cloudflare-dns.com@1.1.1.1", "1.1.1.1 (CloudFlare)")
|
||||
o:value("tls://dns.google@8.8.4.4", "8.8.4.4 (Google)")
|
||||
o:value("tls://dns.google@8.8.8.8", "8.8.8.8 (Google)")
|
||||
o:value("tls://dns.quad9.net@9.9.9.9", "9.9.9.9 (Quad9)")
|
||||
o:value("tls://dns.quad9.net@149.112.112.112", "149.112.112.112 (Quad9)")
|
||||
o:value("tls://dns.adguard.com@94.140.14.14", "94.140.14.14 (AdGuard)")
|
||||
o:value("tls://dns.adguard.com@94.140.15.15", "94.140.15.15 (AdGuard)")
|
||||
o:value("tls://dns.opendns.com@208.67.222.222", "208.67.222.222 (OpenDNS)")
|
||||
o:value("tls://dns.opendns.com@208.67.220.220", "208.67.220.220 (OpenDNS)")
|
||||
o.default = "tls://1.1.1.1"
|
||||
o:value("tls://1.0.0.1", "1.0.0.1 (CloudFlare)")
|
||||
o:value("tls://1.1.1.1", "1.1.1.1 (CloudFlare)")
|
||||
o:value("tls://8.8.4.4", "8.8.4.4 (Google)")
|
||||
o:value("tls://8.8.8.8", "8.8.8.8 (Google)")
|
||||
o:value("tls://9.9.9.9", "9.9.9.9 (Quad9)")
|
||||
o:value("tls://149.112.112.112", "149.112.112.112 (Quad9)")
|
||||
o:value("tls://94.140.14.14", "94.140.14.14 (AdGuard)")
|
||||
o:value("tls://94.140.15.15", "94.140.15.15 (AdGuard)")
|
||||
o:value("tls://208.67.222.222", "208.67.222.222 (OpenDNS)")
|
||||
o:value("tls://208.67.220.220", "208.67.220.220 (OpenDNS)")
|
||||
o.validate = chinadns_dot_validate
|
||||
o:depends("dns_mode", "dot")
|
||||
|
||||
|
@@ -718,7 +718,7 @@ function gen_config(var)
|
||||
local blc_node_tag = "blc-" .. blc_node_id
|
||||
local is_new_blc_node = true
|
||||
for _, outbound in ipairs(outbounds) do
|
||||
if outbound.tag:find("^" .. blc_node_tag) == 1 then
|
||||
if string.sub(outbound.tag, 1, #blc_node_tag) == blc_node_tag then
|
||||
is_new_blc_node = false
|
||||
valid_nodes[#valid_nodes + 1] = outbound.tag
|
||||
break
|
||||
@@ -743,7 +743,7 @@ function gen_config(var)
|
||||
if fallback_node_id then
|
||||
local is_new_node = true
|
||||
for _, outbound in ipairs(outbounds) do
|
||||
if outbound.tag:find("^" .. fallback_node_id) == 1 then
|
||||
if string.sub(outbound.tag, 1, #fallback_node_id) == fallback_node_id then
|
||||
is_new_node = false
|
||||
fallback_node_tag = outbound.tag
|
||||
break
|
||||
|
@@ -919,7 +919,7 @@ run_redir() {
|
||||
_args="${_args} direct_dns_tcp_server=$(config_t_get global direct_dns_tcp 223.5.5.5 | sed 's/:/#/g')"
|
||||
;;
|
||||
dot)
|
||||
local tmp_dot_dns=$(config_t_get global direct_dns_dot "tls://dot.pub@1.12.12.12")
|
||||
local tmp_dot_dns=$(config_t_get global direct_dns_dot "tls://1.12.12.12")
|
||||
local tmp_dot_ip=$(echo "$tmp_dot_dns" | sed -n 's/.*:\/\/\([^@#]*@\)*\([^@#]*\).*/\2/p')
|
||||
local tmp_dot_port=$(echo "$tmp_dot_dns" | sed -n 's/.*#\([0-9]\+\).*/\1/p')
|
||||
_args="${_args} direct_dns_dot_server=$tmp_dot_ip#${tmp_dot_port:-853}"
|
||||
@@ -1397,7 +1397,7 @@ start_dns() {
|
||||
;;
|
||||
dot)
|
||||
if [ "$chinadns_tls" != "nil" ]; then
|
||||
local DIRECT_DNS=$(config_t_get global direct_dns_dot "tls://dot.pub@1.12.12.12")
|
||||
local DIRECT_DNS=$(config_t_get global direct_dns_dot "tls://1.12.12.12")
|
||||
china_ng_local_dns=${DIRECT_DNS}
|
||||
|
||||
#当全局(包括访问控制节点)开启chinadns-ng时,不启动新进程。
|
||||
@@ -1519,7 +1519,7 @@ start_dns() {
|
||||
TCP_PROXY_DNS=1
|
||||
if [ "$chinadns_tls" != "nil" ]; then
|
||||
local china_ng_listen_port=${NEXT_DNS_LISTEN_PORT}
|
||||
local china_ng_trust_dns=$(config_t_get global remote_dns_dot "tls://dns.google@8.8.4.4")
|
||||
local china_ng_trust_dns=$(config_t_get global remote_dns_dot "tls://1.1.1.1")
|
||||
local tmp_dot_ip=$(echo "$china_ng_trust_dns" | sed -n 's/.*:\/\/\([^@#]*@\)*\([^@#]*\).*/\2/p')
|
||||
local tmp_dot_port=$(echo "$china_ng_trust_dns" | sed -n 's/.*#\([0-9]\+\).*/\1/p')
|
||||
REMOTE_DNS="$tmp_dot_ip#${tmp_dot_port:-853}"
|
||||
@@ -1864,7 +1864,7 @@ acl_app() {
|
||||
;;
|
||||
dot)
|
||||
if [ "$(chinadns-ng -V | grep -i wolfssl)" != "nil" ]; then
|
||||
_chinadns_local_dns=$(config_t_get global direct_dns_dot "tls://dot.pub@1.12.12.12")
|
||||
_chinadns_local_dns=$(config_t_get global direct_dns_dot "tls://1.12.12.12")
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
@@ -9,20 +9,21 @@ probe_file="/tmp/etc/passwall/haproxy/Probe_URL"
|
||||
probeUrl="https://www.google.com/generate_204"
|
||||
if [ -f "$probe_file" ]; then
|
||||
firstLine=$(head -n 1 "$probe_file" | tr -d ' \t')
|
||||
if [ -n "$firstLine" ]; then
|
||||
probeUrl="$firstLine"
|
||||
fi
|
||||
[ -n "$firstLine" ] && probeUrl="$firstLine"
|
||||
fi
|
||||
|
||||
status=$(/usr/bin/curl -I -o /dev/null -skL -x socks5h://${server_address}:${server_port} --connect-timeout 3 --retry 3 -w %{http_code} "${probeUrl}")
|
||||
extra_params="-x socks5h://${server_address}:${server_port}"
|
||||
if /usr/bin/curl --help all | grep -q "\-\-retry-all-errors"; then
|
||||
extra_params="${extra_params} --retry-all-errors"
|
||||
fi
|
||||
|
||||
status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout 3 --retry 1 -w "%{http_code}" "${probeUrl}")
|
||||
|
||||
case "$status" in
|
||||
204|\
|
||||
200)
|
||||
status=200
|
||||
200|204)
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
return_code=1
|
||||
if [ "$status" = "200" ]; then
|
||||
return_code=0
|
||||
fi
|
||||
exit ${return_code}
|
||||
|
@@ -28,9 +28,10 @@ test_url() {
|
||||
local timeout=2
|
||||
[ -n "$3" ] && timeout=$3
|
||||
local extra_params=$4
|
||||
curl --help all | grep "\-\-retry-all-errors" > /dev/null
|
||||
[ $? == 0 ] && extra_params="--retry-all-errors ${extra_params}"
|
||||
status=$(/usr/bin/curl -I -o /dev/null -skL --user-agent "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" ${extra_params} --connect-timeout ${timeout} --retry ${try} -w %{http_code} "$url")
|
||||
if /usr/bin/curl --help all | grep -q "\-\-retry-all-errors"; then
|
||||
extra_params="--retry-all-errors ${extra_params}"
|
||||
fi
|
||||
status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout ${timeout} --retry ${try} -w %{http_code} "$url")
|
||||
case "$status" in
|
||||
204)
|
||||
status=200
|
||||
|
@@ -50,17 +50,17 @@ type CacheFile interface {
|
||||
StoreSelected(group string, selected string) error
|
||||
LoadGroupExpand(group string) (isExpand bool, loaded bool)
|
||||
StoreGroupExpand(group string, expand bool) error
|
||||
LoadRuleSet(tag string) *SavedRuleSet
|
||||
SaveRuleSet(tag string, set *SavedRuleSet) error
|
||||
LoadRuleSet(tag string) *SavedBinary
|
||||
SaveRuleSet(tag string, set *SavedBinary) error
|
||||
}
|
||||
|
||||
type SavedRuleSet struct {
|
||||
type SavedBinary struct {
|
||||
Content []byte
|
||||
LastUpdated time.Time
|
||||
LastEtag string
|
||||
}
|
||||
|
||||
func (s *SavedRuleSet) MarshalBinary() ([]byte, error) {
|
||||
func (s *SavedBinary) MarshalBinary() ([]byte, error) {
|
||||
var buffer bytes.Buffer
|
||||
err := binary.Write(&buffer, binary.BigEndian, uint8(1))
|
||||
if err != nil {
|
||||
@@ -81,7 +81,7 @@ func (s *SavedRuleSet) MarshalBinary() ([]byte, error) {
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func (s *SavedRuleSet) UnmarshalBinary(data []byte) error {
|
||||
func (s *SavedBinary) UnmarshalBinary(data []byte) error {
|
||||
reader := bytes.NewReader(data)
|
||||
var version uint8
|
||||
err := binary.Read(reader, binary.BigEndian, &version)
|
||||
|
@@ -30,7 +30,7 @@ func init() {
|
||||
}
|
||||
|
||||
func generateTLSKeyPair(serverName string) error {
|
||||
privateKeyPem, publicKeyPem, err := tls.GenerateKeyPair(time.Now, serverName, time.Now().AddDate(0, flagGenerateTLSKeyPairMonths, 0))
|
||||
privateKeyPem, publicKeyPem, err := tls.GenerateCertificate(nil, nil, time.Now, serverName, time.Now().AddDate(0, flagGenerateTLSKeyPairMonths, 0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@@ -11,8 +11,8 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func GenerateCertificate(timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
|
||||
privateKeyPem, publicKeyPem, err := GenerateKeyPair(timeFunc, serverName, timeFunc().Add(time.Hour))
|
||||
func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
|
||||
privateKeyPem, publicKeyPem, err := GenerateCertificate(parent, parentKey, timeFunc, serverName, timeFunc().Add(time.Hour))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -23,7 +23,7 @@ func GenerateCertificate(timeFunc func() time.Time, serverName string) (*tls.Cer
|
||||
return &certificate, err
|
||||
}
|
||||
|
||||
func GenerateKeyPair(timeFunc func() time.Time, serverName string, expire time.Time) (privateKeyPem []byte, publicKeyPem []byte, err error) {
|
||||
func GenerateCertificate(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string, expire time.Time) (privateKeyPem []byte, publicKeyPem []byte, err error) {
|
||||
if timeFunc == nil {
|
||||
timeFunc = time.Now
|
||||
}
|
||||
@@ -47,7 +47,11 @@ func GenerateKeyPair(timeFunc func() time.Time, serverName string, expire time.T
|
||||
},
|
||||
DNSNames: []string{serverName},
|
||||
}
|
||||
publicDer, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
|
||||
if parent == nil {
|
||||
parent = template
|
||||
parentKey = key
|
||||
}
|
||||
publicDer, err := x509.CreateCertificate(rand.Reader, template, parent, key.Public(), parentKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@@ -222,7 +222,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
|
||||
}
|
||||
if certificate == nil && key == nil && options.Insecure {
|
||||
tlsConfig.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
return GenerateCertificate(ntp.TimeFuncFromContext(ctx), info.ServerName)
|
||||
return GenerateKeyPair(nil, nil, ntp.TimeFuncFromContext(ctx), info.ServerName)
|
||||
}
|
||||
} else {
|
||||
if certificate == nil {
|
||||
|
@@ -284,8 +284,8 @@ func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedRuleSet {
|
||||
var savedSet adapter.SavedRuleSet
|
||||
func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedBinary {
|
||||
var savedSet adapter.SavedBinary
|
||||
err := c.DB.View(func(t *bbolt.Tx) error {
|
||||
bucket := c.bucket(t, bucketRuleSet)
|
||||
if bucket == nil {
|
||||
@@ -303,7 +303,7 @@ func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedRuleSet {
|
||||
return &savedSet
|
||||
}
|
||||
|
||||
func (c *CacheFile) SaveRuleSet(tag string, set *adapter.SavedRuleSet) error {
|
||||
func (c *CacheFile) SaveRuleSet(tag string, set *adapter.SavedBinary) error {
|
||||
return c.DB.Batch(func(t *bbolt.Tx) error {
|
||||
bucket, err := c.createBucket(t, bucketRuleSet)
|
||||
if err != nil {
|
||||
|
@@ -7,11 +7,13 @@ var (
|
||||
|
||||
type Locale struct {
|
||||
// deprecated messages for graphical clients
|
||||
Locale string
|
||||
DeprecatedMessage string
|
||||
DeprecatedMessageNoLink string
|
||||
}
|
||||
|
||||
var defaultLocal = &Locale{
|
||||
Locale: "en_US",
|
||||
DeprecatedMessage: "%s is deprecated in sing-box %s and will be removed in sing-box %s please checkout documentation for migration.",
|
||||
DeprecatedMessageNoLink: "%s is deprecated in sing-box %s and will be removed in sing-box %s.",
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ var warningMessageForEndUsers = "\n\n如果您不明白此消息意味着什么
|
||||
|
||||
func init() {
|
||||
localeRegistry["zh_CN"] = &Locale{
|
||||
Locale: "zh_CN",
|
||||
DeprecatedMessage: "%s 已在 sing-box %s 中被弃用,且将在 sing-box %s 中被移除,请参阅迁移指南。" + warningMessageForEndUsers,
|
||||
DeprecatedMessageNoLink: "%s 已在 sing-box %s 中被弃用,且将在 sing-box %s 中被移除。" + warningMessageForEndUsers,
|
||||
}
|
||||
|
@@ -162,7 +162,7 @@ func (r *Router) Start(stage adapter.StartStage) error {
|
||||
r.started = true
|
||||
return nil
|
||||
case adapter.StartStateStarted:
|
||||
for _, ruleSet := range r.ruleSetMap {
|
||||
for _, ruleSet := range r.ruleSets {
|
||||
ruleSet.Cleanup()
|
||||
}
|
||||
runtime.GC()
|
||||
@@ -180,6 +180,13 @@ func (r *Router) Close() error {
|
||||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
for i, ruleSet := range r.ruleSets {
|
||||
monitor.Start("close rule-set[", i, "]")
|
||||
err = E.Append(err, ruleSet.Close(), func(err error) error {
|
||||
return E.Cause(err, "close rule-set[", i, "]")
|
||||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/fswatch"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@@ -26,14 +27,16 @@ import (
|
||||
var _ adapter.RuleSet = (*LocalRuleSet)(nil)
|
||||
|
||||
type LocalRuleSet struct {
|
||||
ctx context.Context
|
||||
logger logger.Logger
|
||||
tag string
|
||||
rules []adapter.HeadlessRule
|
||||
metadata adapter.RuleSetMetadata
|
||||
fileFormat string
|
||||
watcher *fswatch.Watcher
|
||||
refs atomic.Int32
|
||||
ctx context.Context
|
||||
logger logger.Logger
|
||||
tag string
|
||||
rules []adapter.HeadlessRule
|
||||
metadata adapter.RuleSetMetadata
|
||||
fileFormat string
|
||||
watcher *fswatch.Watcher
|
||||
callbackAccess sync.Mutex
|
||||
callbacks list.List[adapter.RuleSetUpdateCallback]
|
||||
refs atomic.Int32
|
||||
}
|
||||
|
||||
func NewLocalRuleSet(ctx context.Context, logger logger.Logger, options option.RuleSet) (*LocalRuleSet, error) {
|
||||
@@ -52,13 +55,12 @@ func NewLocalRuleSet(ctx context.Context, logger logger.Logger, options option.R
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
err := ruleSet.reloadFile(filemanager.BasePath(ctx, options.LocalOptions.Path))
|
||||
filePath := filemanager.BasePath(ctx, options.LocalOptions.Path)
|
||||
filePath, _ = filepath.Abs(filePath)
|
||||
err := ruleSet.reloadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if options.Type == C.RuleSetTypeLocal {
|
||||
filePath, _ := filepath.Abs(options.LocalOptions.Path)
|
||||
watcher, err := fswatch.NewWatcher(fswatch.Options{
|
||||
Path: []string{filePath},
|
||||
Callback: func(path string) {
|
||||
@@ -141,6 +143,12 @@ func (s *LocalRuleSet) reloadRules(headlessRules []option.HeadlessRule) error {
|
||||
metadata.ContainsIPCIDRRule = hasHeadlessRule(headlessRules, isIPCIDRHeadlessRule)
|
||||
s.rules = rules
|
||||
s.metadata = metadata
|
||||
s.callbackAccess.Lock()
|
||||
callbacks := s.callbacks.Array()
|
||||
s.callbackAccess.Unlock()
|
||||
for _, callback := range callbacks {
|
||||
callback(s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -173,10 +181,15 @@ func (s *LocalRuleSet) Cleanup() {
|
||||
}
|
||||
|
||||
func (s *LocalRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] {
|
||||
return nil
|
||||
s.callbackAccess.Lock()
|
||||
defer s.callbackAccess.Unlock()
|
||||
return s.callbacks.PushBack(callback)
|
||||
}
|
||||
|
||||
func (s *LocalRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) {
|
||||
s.callbackAccess.Lock()
|
||||
defer s.callbackAccess.Unlock()
|
||||
s.callbacks.Remove(element)
|
||||
}
|
||||
|
||||
func (s *LocalRuleSet) Close() error {
|
||||
|
@@ -35,23 +35,23 @@ import (
|
||||
var _ adapter.RuleSet = (*RemoteRuleSet)(nil)
|
||||
|
||||
type RemoteRuleSet struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
outboundManager adapter.OutboundManager
|
||||
logger logger.ContextLogger
|
||||
options option.RuleSet
|
||||
metadata adapter.RuleSetMetadata
|
||||
updateInterval time.Duration
|
||||
dialer N.Dialer
|
||||
rules []adapter.HeadlessRule
|
||||
lastUpdated time.Time
|
||||
lastEtag string
|
||||
updateTicker *time.Ticker
|
||||
cacheFile adapter.CacheFile
|
||||
pauseManager pause.Manager
|
||||
callbackAccess sync.Mutex
|
||||
callbacks list.List[adapter.RuleSetUpdateCallback]
|
||||
refs atomic.Int32
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
logger logger.ContextLogger
|
||||
outbound adapter.OutboundManager
|
||||
options option.RuleSet
|
||||
metadata adapter.RuleSetMetadata
|
||||
updateInterval time.Duration
|
||||
dialer N.Dialer
|
||||
rules []adapter.HeadlessRule
|
||||
lastUpdated time.Time
|
||||
lastEtag string
|
||||
updateTicker *time.Ticker
|
||||
cacheFile adapter.CacheFile
|
||||
pauseManager pause.Manager
|
||||
callbackAccess sync.Mutex
|
||||
callbacks list.List[adapter.RuleSetUpdateCallback]
|
||||
refs atomic.Int32
|
||||
}
|
||||
|
||||
func NewRemoteRuleSet(ctx context.Context, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet {
|
||||
@@ -63,13 +63,13 @@ func NewRemoteRuleSet(ctx context.Context, logger logger.ContextLogger, options
|
||||
updateInterval = 24 * time.Hour
|
||||
}
|
||||
return &RemoteRuleSet{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
outboundManager: service.FromContext[adapter.OutboundManager](ctx),
|
||||
logger: logger,
|
||||
options: options,
|
||||
updateInterval: updateInterval,
|
||||
pauseManager: service.FromContext[pause.Manager](ctx),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
outbound: service.FromContext[adapter.OutboundManager](ctx),
|
||||
logger: logger,
|
||||
options: options,
|
||||
updateInterval: updateInterval,
|
||||
pauseManager: service.FromContext[pause.Manager](ctx),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,13 +85,13 @@ func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext *adapter.
|
||||
s.cacheFile = service.FromContext[adapter.CacheFile](s.ctx)
|
||||
var dialer N.Dialer
|
||||
if s.options.RemoteOptions.DownloadDetour != "" {
|
||||
outbound, loaded := s.outboundManager.Outbound(s.options.RemoteOptions.DownloadDetour)
|
||||
outbound, loaded := s.outbound.Outbound(s.options.RemoteOptions.DownloadDetour)
|
||||
if !loaded {
|
||||
return E.New("download_detour not found: ", s.options.RemoteOptions.DownloadDetour)
|
||||
return E.New("download detour not found: ", s.options.RemoteOptions.DownloadDetour)
|
||||
}
|
||||
dialer = outbound
|
||||
} else {
|
||||
dialer = s.outboundManager.Default()
|
||||
dialer = s.outbound.Default()
|
||||
}
|
||||
s.dialer = dialer
|
||||
if s.cacheFile != nil {
|
||||
@@ -292,7 +292,7 @@ func (s *RemoteRuleSet) fetchOnce(ctx context.Context, startContext *adapter.HTT
|
||||
}
|
||||
s.lastUpdated = time.Now()
|
||||
if s.cacheFile != nil {
|
||||
err = s.cacheFile.SaveRuleSet(s.options.Tag, &adapter.SavedRuleSet{
|
||||
err = s.cacheFile.SaveRuleSet(s.options.Tag, &adapter.SavedBinary{
|
||||
LastUpdated: s.lastUpdated,
|
||||
Content: content,
|
||||
LastEtag: s.lastEtag,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_VERSION:=1.17.4
|
||||
PKG_VERSION:=1.17.5
|
||||
|
||||
LUCI_TITLE:=LuCI Support for mihomo
|
||||
LUCI_DEPENDS:=+luci-base +mihomo
|
||||
|
@@ -155,6 +155,16 @@ return view.extend({
|
||||
o.retain = true;
|
||||
o.depends('tun_gso', '1');
|
||||
|
||||
o = s.taboption('tun', form.Flag, 'tun_dns_hijack', '*' + ' ' + _('Overwrite DNS Hijack'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tun', form.DynamicList, 'tun_dns_hijacks', '*' + ' ' + _('Edit DNS Hijacks'));
|
||||
o.retain = true;
|
||||
o.rmempty = false;
|
||||
o.depends('tun_dns_hijack', '1');
|
||||
o.value('tcp://any:53');
|
||||
o.value('udp://any:53');
|
||||
|
||||
o = s.taboption('tun', form.Flag, 'tun_endpoint_independent_nat', '*' + ' ' + _('Endpoint Independent NAT'));
|
||||
o.rmempty = false;
|
||||
|
||||
|
@@ -26,7 +26,7 @@ msgstr ""
|
||||
msgid "Allow Lan"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:190
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:198
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/proxy.js:65
|
||||
msgid "Allow Mode"
|
||||
msgstr ""
|
||||
@@ -48,7 +48,7 @@ msgstr ""
|
||||
msgid "Auto"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:189
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:197
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/proxy.js:66
|
||||
msgid "Block Mode"
|
||||
msgstr ""
|
||||
@@ -111,15 +111,15 @@ msgstr ""
|
||||
msgid "Cron Expression"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:161
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:169
|
||||
msgid "DNS Config"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:167
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:175
|
||||
msgid "DNS Mode"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:163
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:171
|
||||
msgid "DNS Port"
|
||||
msgstr ""
|
||||
|
||||
@@ -159,11 +159,11 @@ msgstr ""
|
||||
msgid "Disable Safe Path Check"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:201
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:209
|
||||
msgid "DoH Prefer HTTP/3"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:227
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:235
|
||||
msgid "Domain Name"
|
||||
msgstr ""
|
||||
|
||||
@@ -171,19 +171,23 @@ msgstr ""
|
||||
msgid "Edit Authentications"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:183
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:161
|
||||
msgid "Edit DNS Hijacks"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:191
|
||||
msgid "Edit Fake-IP Filters"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:216
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:224
|
||||
msgid "Edit Hosts"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:258
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:266
|
||||
msgid "Edit Nameserver Policies"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:235
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:243
|
||||
msgid "Edit Nameservers"
|
||||
msgstr ""
|
||||
|
||||
@@ -200,17 +204,17 @@ msgstr ""
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:23
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:44
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:125
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:224
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:243
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:266
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:276
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:308
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:356
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:232
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:251
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:274
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:284
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:316
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:364
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/proxy.js:31
|
||||
msgid "Enable"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:158
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:166
|
||||
msgid "Endpoint Independent NAT"
|
||||
msgstr ""
|
||||
|
||||
@@ -222,15 +226,15 @@ msgstr ""
|
||||
msgid "External Control Config"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:193
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:201
|
||||
msgid "Fake-IP Cache"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:187
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:195
|
||||
msgid "Fake-IP Filter Mode"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:172
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:180
|
||||
msgid "Fake-IP Range"
|
||||
msgstr ""
|
||||
|
||||
@@ -255,7 +259,7 @@ msgstr ""
|
||||
msgid "File:"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:291
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:299
|
||||
msgid "Force Sniff Domain Name"
|
||||
msgstr ""
|
||||
|
||||
@@ -271,39 +275,39 @@ msgstr ""
|
||||
msgid "General Config"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:329
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:337
|
||||
msgid "GeoData Loader"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:325
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:333
|
||||
msgid "GeoIP Format"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:342
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:350
|
||||
msgid "GeoIP(ASN) Url"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:339
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:347
|
||||
msgid "GeoIP(DAT) Url"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:336
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:344
|
||||
msgid "GeoIP(MMDB) Url"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:333
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:341
|
||||
msgid "GeoSite Url"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:345
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:353
|
||||
msgid "GeoX Auto Update"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:323
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:331
|
||||
msgid "GeoX Config"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:348
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:356
|
||||
msgid "GeoX Update Interval"
|
||||
msgstr ""
|
||||
|
||||
@@ -323,7 +327,7 @@ msgstr ""
|
||||
msgid "How To Use"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:230
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:238
|
||||
msgid "IP"
|
||||
msgstr ""
|
||||
|
||||
@@ -336,7 +340,7 @@ msgid "IPv4 Proxy"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:50
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:204
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:212
|
||||
msgid "IPv6"
|
||||
msgstr ""
|
||||
|
||||
@@ -348,7 +352,7 @@ msgstr ""
|
||||
msgid "IPv6 Proxy"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:297
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:305
|
||||
msgid "Ignore Sniff Domain Name"
|
||||
msgstr ""
|
||||
|
||||
@@ -385,11 +389,11 @@ msgstr ""
|
||||
msgid "Match Process"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:269
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:277
|
||||
msgid "Matcher"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:331
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:339
|
||||
msgid "Memory Conservative Loader"
|
||||
msgstr ""
|
||||
|
||||
@@ -407,7 +411,7 @@ msgstr ""
|
||||
msgid "Mixin Config"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:354
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:362
|
||||
msgid "Mixin File Content"
|
||||
msgstr ""
|
||||
|
||||
@@ -420,8 +424,8 @@ msgstr ""
|
||||
msgid "Mode"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:253
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:272
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:261
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:280
|
||||
msgid "Nameserver"
|
||||
msgstr ""
|
||||
|
||||
@@ -441,36 +445,40 @@ msgstr ""
|
||||
msgid "Overwrite Authentication"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:285
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:320
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:158
|
||||
msgid "Overwrite DNS Hijack"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:293
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:328
|
||||
msgid "Overwrite Destination"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:178
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:186
|
||||
msgid "Overwrite Fake-IP Filter"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:288
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:296
|
||||
msgid "Overwrite Force Sniff Domain Name"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:213
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:221
|
||||
msgid "Overwrite Hosts"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:294
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:302
|
||||
msgid "Overwrite Ignore Sniff Domain Name"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:232
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:240
|
||||
msgid "Overwrite Nameserver"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:255
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:263
|
||||
msgid "Overwrite Nameserver Policy"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:300
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:308
|
||||
msgid "Overwrite Sniff By Protocol"
|
||||
msgstr ""
|
||||
|
||||
@@ -478,11 +486,11 @@ msgstr ""
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:356
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:364
|
||||
msgid "Please go to the editor tab to edit the file for mixin"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:317
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:325
|
||||
msgid "Port"
|
||||
msgstr ""
|
||||
|
||||
@@ -499,7 +507,7 @@ msgstr ""
|
||||
msgid "Profile for Startup"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:311
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:319
|
||||
msgid "Protocol"
|
||||
msgstr ""
|
||||
|
||||
@@ -524,7 +532,7 @@ msgstr ""
|
||||
msgid "Remote"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:198
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:206
|
||||
msgid "Respect Rules"
|
||||
msgstr ""
|
||||
|
||||
@@ -561,24 +569,24 @@ msgstr ""
|
||||
msgid "Scroll To Bottom"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/tools/mihomo.js:99
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/tools/mihomo.js:117
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/tools/mihomo.js:100
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/tools/mihomo.js:118
|
||||
msgid "Service is not running."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:303
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:311
|
||||
msgid "Sniff By Protocol"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:282
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:290
|
||||
msgid "Sniff Pure IP"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:279
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:287
|
||||
msgid "Sniff Redir-Host"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:274
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:282
|
||||
msgid "Sniffer Config"
|
||||
msgstr ""
|
||||
|
||||
@@ -586,7 +594,7 @@ msgstr ""
|
||||
msgid "Stack"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:330
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:338
|
||||
msgid "Standard Loader"
|
||||
msgstr ""
|
||||
|
||||
@@ -665,7 +673,7 @@ msgstr ""
|
||||
msgid "Transparent Proxy with Mihomo on OpenWrt."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:246
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:254
|
||||
msgid "Type"
|
||||
msgstr ""
|
||||
|
||||
@@ -706,11 +714,11 @@ msgstr ""
|
||||
msgid "Upload Profile"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:210
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:218
|
||||
msgid "Use Hosts"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:207
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:215
|
||||
msgid "Use System Hosts"
|
||||
msgstr ""
|
||||
|
||||
|
@@ -33,7 +33,7 @@ msgstr "全部端口"
|
||||
msgid "Allow Lan"
|
||||
msgstr "允许局域网访问"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:190
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:198
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/proxy.js:65
|
||||
msgid "Allow Mode"
|
||||
msgstr "白名单模式"
|
||||
@@ -55,7 +55,7 @@ msgstr "插件版本"
|
||||
msgid "Auto"
|
||||
msgstr "自动"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:189
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:197
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/proxy.js:66
|
||||
msgid "Block Mode"
|
||||
msgstr "黑名单模式"
|
||||
@@ -118,15 +118,15 @@ msgstr "核心版本"
|
||||
msgid "Cron Expression"
|
||||
msgstr "Cron 表达式"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:161
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:169
|
||||
msgid "DNS Config"
|
||||
msgstr "DNS 配置"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:167
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:175
|
||||
msgid "DNS Mode"
|
||||
msgstr "DNS 模式"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:163
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:171
|
||||
msgid "DNS Port"
|
||||
msgstr "DNS 端口"
|
||||
|
||||
@@ -166,11 +166,11 @@ msgstr "禁用回环检测"
|
||||
msgid "Disable Safe Path Check"
|
||||
msgstr "禁用安全路径检查"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:201
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:209
|
||||
msgid "DoH Prefer HTTP/3"
|
||||
msgstr "DoH 优先 HTTP/3"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:227
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:235
|
||||
msgid "Domain Name"
|
||||
msgstr "域名"
|
||||
|
||||
@@ -178,19 +178,23 @@ msgstr "域名"
|
||||
msgid "Edit Authentications"
|
||||
msgstr "编辑身份验证"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:183
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:161
|
||||
msgid "Edit DNS Hijacks"
|
||||
msgstr "编辑 DNS 劫持"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:191
|
||||
msgid "Edit Fake-IP Filters"
|
||||
msgstr "编辑 Fake-IP 过滤列表"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:216
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:224
|
||||
msgid "Edit Hosts"
|
||||
msgstr "编辑 Hosts"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:258
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:266
|
||||
msgid "Edit Nameserver Policies"
|
||||
msgstr "编辑 DNS 服务器查询策略"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:235
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:243
|
||||
msgid "Edit Nameservers"
|
||||
msgstr "编辑 DNS 服务器"
|
||||
|
||||
@@ -207,17 +211,17 @@ msgstr "编辑器"
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:23
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:44
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:125
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:224
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:243
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:266
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:276
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:308
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:356
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:232
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:251
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:274
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:284
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:316
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:364
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/proxy.js:31
|
||||
msgid "Enable"
|
||||
msgstr "启用"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:158
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:166
|
||||
msgid "Endpoint Independent NAT"
|
||||
msgstr "独立于端点的 NAT"
|
||||
|
||||
@@ -229,15 +233,15 @@ msgstr "到期时间"
|
||||
msgid "External Control Config"
|
||||
msgstr "外部控制配置"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:193
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:201
|
||||
msgid "Fake-IP Cache"
|
||||
msgstr "Fake-IP 缓存"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:187
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:195
|
||||
msgid "Fake-IP Filter Mode"
|
||||
msgstr "Fake-IP 过滤模式"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:172
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:180
|
||||
msgid "Fake-IP Range"
|
||||
msgstr "Fake-IP 范围"
|
||||
|
||||
@@ -262,7 +266,7 @@ msgstr "IPv6 保留地址"
|
||||
msgid "File:"
|
||||
msgstr "文件:"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:291
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:299
|
||||
msgid "Force Sniff Domain Name"
|
||||
msgstr "强制嗅探的域名"
|
||||
|
||||
@@ -278,39 +282,39 @@ msgstr "分段最大长度"
|
||||
msgid "General Config"
|
||||
msgstr "全局配置"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:329
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:337
|
||||
msgid "GeoData Loader"
|
||||
msgstr "GeoData 加载器"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:325
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:333
|
||||
msgid "GeoIP Format"
|
||||
msgstr "GeoIP 格式"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:342
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:350
|
||||
msgid "GeoIP(ASN) Url"
|
||||
msgstr "GeoIP(ASN) 下载地址"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:339
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:347
|
||||
msgid "GeoIP(DAT) Url"
|
||||
msgstr "GeoIP(DAT) 下载地址"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:336
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:344
|
||||
msgid "GeoIP(MMDB) Url"
|
||||
msgstr "GeoIP(MMDB) 下载地址"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:333
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:341
|
||||
msgid "GeoSite Url"
|
||||
msgstr "GeoSite 下载地址"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:345
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:353
|
||||
msgid "GeoX Auto Update"
|
||||
msgstr "定时更新GeoX文件"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:323
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:331
|
||||
msgid "GeoX Config"
|
||||
msgstr "GeoX 配置"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:348
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:356
|
||||
msgid "GeoX Update Interval"
|
||||
msgstr "GeoX 文件更新间隔"
|
||||
|
||||
@@ -330,7 +334,7 @@ msgstr "HTTP 端口"
|
||||
msgid "How To Use"
|
||||
msgstr "使用说明"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:230
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:238
|
||||
msgid "IP"
|
||||
msgstr ""
|
||||
|
||||
@@ -343,7 +347,7 @@ msgid "IPv4 Proxy"
|
||||
msgstr "IPv4 代理"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:50
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:204
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:212
|
||||
msgid "IPv6"
|
||||
msgstr ""
|
||||
|
||||
@@ -355,7 +359,7 @@ msgstr "IPv6 DNS 劫持"
|
||||
msgid "IPv6 Proxy"
|
||||
msgstr "IPv6 代理"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:297
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:305
|
||||
msgid "Ignore Sniff Domain Name"
|
||||
msgstr "忽略嗅探的域名"
|
||||
|
||||
@@ -392,11 +396,11 @@ msgstr "最大传输单元"
|
||||
msgid "Match Process"
|
||||
msgstr "匹配进程"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:269
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:277
|
||||
msgid "Matcher"
|
||||
msgstr "匹配"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:331
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:339
|
||||
msgid "Memory Conservative Loader"
|
||||
msgstr "为内存受限设备优化的加载器"
|
||||
|
||||
@@ -414,7 +418,7 @@ msgstr "混合端口"
|
||||
msgid "Mixin Config"
|
||||
msgstr "混入配置"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:354
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:362
|
||||
msgid "Mixin File Content"
|
||||
msgstr "混入文件内容"
|
||||
|
||||
@@ -427,8 +431,8 @@ msgstr "混入选项"
|
||||
msgid "Mode"
|
||||
msgstr "模式"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:253
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:272
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:261
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:280
|
||||
msgid "Nameserver"
|
||||
msgstr "DNS 服务器"
|
||||
|
||||
@@ -448,36 +452,40 @@ msgstr "出站接口"
|
||||
msgid "Overwrite Authentication"
|
||||
msgstr "覆盖身份验证"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:285
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:320
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:158
|
||||
msgid "Overwrite DNS Hijack"
|
||||
msgstr "覆盖 DNS 劫持"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:293
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:328
|
||||
msgid "Overwrite Destination"
|
||||
msgstr "将嗅探结果作为连接目标"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:178
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:186
|
||||
msgid "Overwrite Fake-IP Filter"
|
||||
msgstr "覆盖 Fake-IP 过滤列表"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:288
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:296
|
||||
msgid "Overwrite Force Sniff Domain Name"
|
||||
msgstr "覆盖强制嗅探的域名"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:213
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:221
|
||||
msgid "Overwrite Hosts"
|
||||
msgstr "覆盖 Hosts"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:294
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:302
|
||||
msgid "Overwrite Ignore Sniff Domain Name"
|
||||
msgstr "覆盖忽略嗅探的域名"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:232
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:240
|
||||
msgid "Overwrite Nameserver"
|
||||
msgstr "覆盖 DNS 服务器"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:255
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:263
|
||||
msgid "Overwrite Nameserver Policy"
|
||||
msgstr "覆盖 DNS 服务器查询策略"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:300
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:308
|
||||
msgid "Overwrite Sniff By Protocol"
|
||||
msgstr "覆盖按协议嗅探"
|
||||
|
||||
@@ -485,11 +493,11 @@ msgstr "覆盖按协议嗅探"
|
||||
msgid "Password"
|
||||
msgstr "密码"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:356
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:364
|
||||
msgid "Please go to the editor tab to edit the file for mixin"
|
||||
msgstr "请前往编辑器标签编辑用于混入的文件"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:317
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:325
|
||||
msgid "Port"
|
||||
msgstr "端口"
|
||||
|
||||
@@ -506,7 +514,7 @@ msgstr "配置文件"
|
||||
msgid "Profile for Startup"
|
||||
msgstr "用于启动的配置文件"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:311
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:319
|
||||
msgid "Protocol"
|
||||
msgstr "协议"
|
||||
|
||||
@@ -531,7 +539,7 @@ msgstr "重载服务"
|
||||
msgid "Remote"
|
||||
msgstr "远程"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:198
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:206
|
||||
msgid "Respect Rules"
|
||||
msgstr "遵循分流规则"
|
||||
|
||||
@@ -568,24 +576,24 @@ msgstr "定时重启"
|
||||
msgid "Scroll To Bottom"
|
||||
msgstr "滚动到底部"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/tools/mihomo.js:99
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/tools/mihomo.js:117
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/tools/mihomo.js:100
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/tools/mihomo.js:118
|
||||
msgid "Service is not running."
|
||||
msgstr "服务未在运行。"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:303
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:311
|
||||
msgid "Sniff By Protocol"
|
||||
msgstr "按协议嗅探"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:282
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:290
|
||||
msgid "Sniff Pure IP"
|
||||
msgstr "嗅探纯 IP 连接"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:279
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:287
|
||||
msgid "Sniff Redir-Host"
|
||||
msgstr "嗅探 Redir-Host 流量"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:274
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:282
|
||||
msgid "Sniffer Config"
|
||||
msgstr "嗅探器配置"
|
||||
|
||||
@@ -593,7 +601,7 @@ msgstr "嗅探器配置"
|
||||
msgid "Stack"
|
||||
msgstr "栈"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:330
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:338
|
||||
msgid "Standard Loader"
|
||||
msgstr "标准加载器"
|
||||
|
||||
@@ -672,7 +680,7 @@ msgstr "透明代理"
|
||||
msgid "Transparent Proxy with Mihomo on OpenWrt."
|
||||
msgstr "在 OpenWrt 上使用 Mihomo 进行透明代理。"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:246
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:254
|
||||
msgid "Type"
|
||||
msgstr "类型"
|
||||
|
||||
@@ -713,11 +721,11 @@ msgstr "更新面板"
|
||||
msgid "Upload Profile"
|
||||
msgstr "上传配置文件"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:210
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:218
|
||||
msgid "Use Hosts"
|
||||
msgstr "使用 Hosts"
|
||||
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:207
|
||||
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:215
|
||||
msgid "Use System Hosts"
|
||||
msgstr "使用系统的 Hosts"
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user