Update On Sun Feb 2 19:31:29 CET 2025

This commit is contained in:
github-action[bot]
2025-02-02 19:31:29 +01:00
parent 5fbdd9228c
commit 10c74b79e1
210 changed files with 6548 additions and 8492 deletions

1
.github/update.log vendored
View File

@@ -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 Thu Jan 30 19:32:29 CET 2025
Update On Fri Jan 31 19:32:11 CET 2025 Update On Fri Jan 31 19:32:11 CET 2025
Update On Sat Feb 1 19:32:16 CET 2025 Update On Sat Feb 1 19:32:16 CET 2025
Update On Sun Feb 2 19:31:20 CET 2025

View File

@@ -46,6 +46,12 @@ export default {
], ],
}, },
], ],
'at-rule-no-deprecated': [
true,
{
ignoreAtRules: ['apply'],
},
],
}, },
overrides: [ overrides: [
{ {

File diff suppressed because it is too large Load Diff

View File

@@ -16,12 +16,17 @@ name = "nyanpasu-network-statistic-widget-small"
path = "./src/small.rs" path = "./src/small.rs"
[dependencies] [dependencies]
eframe = "0.29.1" eframe = "0.30"
egui_extras = { version = "0.29", features = ["all_loaders"] } egui_extras = { version = "0.30", features = ["all_loaders"] }
parking_lot = "0.12" parking_lot = "0.12"
image = { version = "0.25.5", features = ["jpeg", "png"] } image = { version = "0.25", features = ["jpeg", "png"] }
humansize = "2.1.3" humansize = "2"
# for svg currentColor replacement # for svg currentColor replacement
resvg = "0.44.0" # for svg rendering resvg = "0.44" # for svg rendering
usvg = "0.44.0" # for svg parsing usvg = "0.44" # for svg parsing
csscolorparser = "0.7.0" # for color conversion 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"] }

View 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)
}

View File

@@ -1,2 +1,5 @@
#![feature(trait_alias)]
pub mod ipc;
mod utils; mod utils;
pub mod widget; pub mod widget;

View File

@@ -3,3 +3,35 @@ pub mod network_statistic_small;
pub use network_statistic_large::NyanpasuNetworkStatisticLargeWidget; pub use network_statistic_large::NyanpasuNetworkStatisticLargeWidget;
pub use network_statistic_small::NyanpasuNetworkStatisticSmallWidget; 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(),
}
}

View File

@@ -1,10 +1,14 @@
use std::sync::Arc;
use std::sync::LazyLock; use std::sync::LazyLock;
use std::sync::OnceLock;
use crate::ipc::Message;
use crate::utils::svg::{render_svg_with_current_color_replace, SvgExt}; use crate::utils::svg::{render_svg_with_current_color_replace, SvgExt};
use eframe::egui::{ use eframe::egui::{
self, style::Selection, Color32, Id, Image, Layout, Margin, Rounding, Sense, Stroke, Style, self, style::Selection, Color32, Id, Image, Layout, Margin, Rounding, Sense, Stroke, Style,
TextureOptions, Theme, Vec2, ViewportCommand, Visuals, TextureOptions, Theme, Vec2, ViewportCommand, Visuals,
}; };
use parking_lot::RwLock;
// Presets // Presets
const STATUS_ICON_CONTAINER_WIDTH: f32 = 20.0; const STATUS_ICON_CONTAINER_WIDTH: f32 = 20.0;
@@ -42,7 +46,9 @@ fn setup_fonts(ctx: &egui::Context) {
fonts.font_data.insert( fonts.font_data.insert(
"Inter".to_owned(), "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 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 { pub enum LogoPreset {
#[default] #[default]
Default, Default,
@@ -93,30 +100,20 @@ pub enum LogoPreset {
Tun, Tun,
} }
#[derive(Debug, Default)]
pub struct StatisticMessage {
download_total: u64,
upload_total: u64,
download_speed: u64,
upload_speed: u64,
}
#[derive(Debug)] #[derive(Debug)]
pub enum Message { pub struct NyanpasuNetworkStatisticLargeWidgetInner {
UpdateStatistic(StatisticMessage), // data fields
UpdateLogo(LogoPreset),
}
#[derive(Debug)]
pub struct NyanpasuNetworkStatisticLargeWidget {
logo_preset: LogoPreset, logo_preset: LogoPreset,
download_total: u64, download_total: u64,
upload_total: u64, upload_total: u64,
download_speed: u64, download_speed: u64,
upload_speed: u64, upload_speed: u64,
// eframe ctx
egui_ctx: OnceLock<egui::Context>,
} }
impl Default for NyanpasuNetworkStatisticLargeWidget { impl Default for NyanpasuNetworkStatisticLargeWidgetInner {
fn default() -> Self { fn default() -> Self {
Self { Self {
logo_preset: LogoPreset::Default, logo_preset: LogoPreset::Default,
@@ -124,6 +121,22 @@ impl Default for NyanpasuNetworkStatisticLargeWidget {
upload_total: 0, upload_total: 0,
download_speed: 0, download_speed: 0,
upload_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_fonts(&cc.egui_ctx);
setup_custom_style(&cc.egui_ctx); setup_custom_style(&cc.egui_ctx);
egui_extras::install_image_loaders(&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 { match msg {
Message::UpdateStatistic(statistic) => { Message::UpdateStatistic(statistic) => {
self.download_total = statistic.download_total; this.download_total = statistic.download_total;
self.upload_total = statistic.upload_total; this.upload_total = statistic.upload_total;
self.download_speed = statistic.download_speed; this.download_speed = statistic.download_speed;
self.upload_speed = statistic.upload_speed; this.upload_speed = statistic.upload_speed;
true
} }
Message::UpdateLogo(logo_preset) => { Message::UpdateLogo(logo_preset) => {
self.logo_preset = logo_preset; this.logo_preset = logo_preset;
true
} }
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) { 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; let visuals = &ctx.style().visuals;
egui::CentralPanel::default() egui::CentralPanel::default()
@@ -229,7 +296,7 @@ impl eframe::App for NyanpasuNetworkStatisticLargeWidget {
let width = ui.available_width(); let width = ui.available_width();
let height = ui.available_height(); let height = ui.available_height();
ui.allocate_ui_with_layout(egui::Vec2::new(width, height), Layout::centered_and_justified(egui::Direction::TopDown), |ui| { 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 width = ui.available_width();
let height = ui.available_height(); let height = ui.available_height();
ui.allocate_ui_with_layout(egui::Vec2::new(width, height), Layout::centered_and_justified(egui::Direction::TopDown), |ui| { 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 width = ui.available_width();
let height = ui.available_height(); let height = ui.available_height();
ui.allocate_ui_with_layout(egui::Vec2::new(width, height), Layout::centered_and_justified(egui::Direction::TopDown), |ui| { 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 width = ui.available_width();
let height = ui.available_height(); let height = ui.available_height();
ui.allocate_ui_with_layout(egui::Vec2::new(width, height), Layout::centered_and_justified(egui::Direction::TopDown), |ui| { 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)));
}); });
}); });
}) })

View File

@@ -1,9 +1,14 @@
use std::sync::Arc;
use std::sync::LazyLock; use std::sync::LazyLock;
use std::sync::OnceLock;
use eframe::egui::{ use eframe::egui::{
self, include_image, style::Selection, Color32, Id, Image, Layout, Margin, RichText, Rounding, self, include_image, style::Selection, Color32, Id, Image, Layout, Margin, RichText, Rounding,
Sense, Stroke, Style, Theme, Vec2, ViewportCommand, Visuals, WidgetText, Sense, Stroke, Style, Theme, Vec2, ViewportCommand, Visuals, WidgetText,
}; };
use parking_lot::RwLock;
use crate::ipc::Message;
// Presets // Presets
const STATUS_ICON_CONTAINER_WIDTH: f32 = 20.0; const STATUS_ICON_CONTAINER_WIDTH: f32 = 20.0;
@@ -36,7 +41,9 @@ fn setup_fonts(ctx: &egui::Context) {
fonts.font_data.insert( fonts.font_data.insert(
"Inter".to_owned(), "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 fonts
@@ -79,19 +86,105 @@ fn use_dark_purple_accent(style: &mut Style) {
}; };
} }
#[derive(Clone)]
pub struct NyanpasuNetworkStatisticSmallWidget { 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 { impl NyanpasuNetworkStatisticSmallWidget {
pub fn new(cc: &eframe::CreationContext<'_>) -> Self { 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_fonts(&cc.egui_ctx);
setup_custom_style(&cc.egui_ctx); setup_custom_style(&cc.egui_ctx);
egui_extras::install_image_loaders(&cc.egui_ctx); egui_extras::install_image_loaders(&cc.egui_ctx);
Self { let rx = crate::ipc::setup_ipc_receiver_with_env().unwrap();
demo_size: 100_000_000, 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) { fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
let visuals = &ctx.style().visuals; 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() egui::CentralPanel::default()
.frame( .frame(
@@ -153,7 +249,10 @@ impl eframe::App for NyanpasuNetworkStatisticSmallWidget {
ui.label( ui.label(
WidgetText::from(RichText::new(format!( WidgetText::from(RichText::new(format!(
"{}/s", "{}/s",
humansize::format_size(self.demo_size, humansize::DECIMAL) humansize::format_size(
this.upload_speed,
humansize::DECIMAL
)
))) )))
.color(LIGHT_MODE_TEXT_COLOR), .color(LIGHT_MODE_TEXT_COLOR),
); );
@@ -166,7 +265,10 @@ impl eframe::App for NyanpasuNetworkStatisticSmallWidget {
ui.label( ui.label(
WidgetText::from(RichText::new(format!( WidgetText::from(RichText::new(format!(
"{}/s", "{}/s",
humansize::format_size(self.demo_size, humansize::DECIMAL) humansize::format_size(
this.download_speed,
humansize::DECIMAL
)
))) )))
.color(LIGHT_MODE_TEXT_COLOR), .color(LIGHT_MODE_TEXT_COLOR),
); );

View File

@@ -16,7 +16,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.0.1", features = [] } tauri-build = { version = "2.0.1", features = [] }
serde = "1" serde = "1"
simd-json = "0.14.1" serde_json = { version = "1.0", features = ["preserve_order"] }
chrono = "0.4" chrono = "0.4"
rustc_version = "0.4" rustc_version = "0.4"
semver = "1.0" semver = "1.0"
@@ -31,10 +31,12 @@ nyanpasu-macro = { path = "../nyanpasu-macro" }
nyanpasu-utils = { git = "https://github.com/libnyanpasu/nyanpasu-utils.git", features = [ nyanpasu-utils = { git = "https://github.com/libnyanpasu/nyanpasu-utils.git", features = [
"specta", "specta",
] } ] }
nyanpasu-egui = { path = "../nyanpasu-egui" }
# Common Utilities # Common Utilities
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
futures = "0.3" futures = "0.3"
futures-util = "0.3"
glob = "0.3.1" glob = "0.3.1"
timeago = "0.4" timeago = "0.4"
humansize = "2.1.3" humansize = "2.1.3"
@@ -47,13 +49,14 @@ async-trait = "0.1.77"
dyn-clone = "1.0.16" dyn-clone = "1.0.16"
thiserror = { workspace = true } thiserror = { workspace = true }
parking_lot = { version = "0.12.1" } parking_lot = { version = "0.12.1" }
itertools = "0.14" # sweet iterator utilities itertools = "0.14" # sweet iterator utilities
rayon = "1.10" # for iterator parallel processing rayon = "1.10" # for iterator parallel processing
ambassador = "0.4.1" # for trait delegation ambassador = "0.4.1" # for trait delegation
derive_builder = "0.20" # for builder pattern derive_builder = "0.20" # for builder pattern
strum = { version = "0.26", features = ["derive"] } # for enum string conversion strum = { version = "0.26", features = ["derive"] } # for enum string conversion
atomic_enum = "0.3.0" # for atomic enum atomic_enum = "0.3.0" # for atomic enum
enumflags2 = "0.7" # for enum flags enumflags2 = "0.7" # for enum flags
backon = { version = "1.0.1", features = ["tokio-sleep"] } # for backoff retry
# Data Structures # Data Structures
dashmap = "6" dashmap = "6"
@@ -61,7 +64,7 @@ indexmap = { version = "2.2.3", features = ["serde"] }
bimap = "0.6.3" bimap = "0.6.3"
# Terminal Utilities # Terminal Utilities
ansi-str = "0.8" # for ansi str stripped ansi-str = "0.9" # for ansi str stripped
ctrlc = "3.4.2" ctrlc = "3.4.2"
colored = "3" colored = "3"
clap = { version = "4.5.4", features = ["derive"] } clap = { version = "4.5.4", features = ["derive"] }
@@ -81,17 +84,17 @@ axum = "0.8"
url = "2" url = "2"
mime = "0.3" mime = "0.3"
reqwest = { version = "0.12", features = ["json", "stream"] } reqwest = { version = "0.12", features = ["json", "stream"] }
tokio-tungstenite = "0.26.1"
urlencoding = "2.1"
port_scanner = "0.1.5" port_scanner = "0.1.5"
sysproxy = { git = "https://github.com/libnyanpasu/sysproxy-rs.git", version = "0.3" } sysproxy = { git = "https://github.com/libnyanpasu/sysproxy-rs.git", version = "0.3" }
backon = { version = "1.0.1", features = ["tokio-sleep"] }
# Serialization # Serialization
serde = { version = "1.0", features = ["derive"] } 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 = [ serde_yaml = { version = "0.10", package = "serde_yaml_ng", branch = "feat/specta", git = "https://github.com/libnyanpasu/serde-yaml-ng.git", features = [
"specta", "specta",
] } ] }
simd-json = "0.14.1"
bincode = "1" bincode = "1"
bytes = { version = "1", features = ["serde"] } bytes = { version = "1", features = ["serde"] }
semver = "1.0" semver = "1.0"

View File

@@ -20,12 +20,12 @@ struct GitInfo {
fn main() { fn main() {
let version: String = if let Ok(true) = exists("../../package.json") { let version: String = if let Ok(true) = exists("../../package.json") {
let mut raw = read("../../package.json").unwrap(); let raw = read("../../package.json").unwrap();
let pkg_json: PackageJson = simd_json::from_slice(&mut raw).unwrap(); let pkg_json: PackageJson = serde_json::from_slice(&raw).unwrap();
pkg_json.version pkg_json.version
} else { } else {
let mut raw = read("./tauri.conf.json").unwrap(); // TODO: fix it when windows arm64 need it let 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 tauri_json: PackageJson = serde_json::from_slice(&raw).unwrap();
tauri_json.version tauri_json.version
}; };
let version = semver::Version::parse(&version).unwrap(); let version = semver::Version::parse(&version).unwrap();
@@ -34,8 +34,8 @@ fn main() {
// Git Information // Git Information
let (commit_hash, commit_author, commit_date) = if let Ok(true) = exists("./tmp/git-info.json") 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 = read("./tmp/git-info.json").unwrap();
let git_info: GitInfo = simd_json::from_slice(&mut git_info).unwrap(); let git_info: GitInfo = serde_json::from_slice(&git_info).unwrap();
(git_info.hash, git_info.author, git_info.time) (git_info.hash, git_info.author, git_info.time)
} else { } else {
let output = Command::new("git") let output = Command::new("git")

File diff suppressed because one or more lines are too long

View File

@@ -1724,6 +1724,14 @@
"name" "name"
], ],
"properties": { "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": { "args": {
"description": "The allowed arguments for the command execution.", "description": "The allowed arguments for the command execution.",
"allOf": [ "allOf": [
@@ -1731,14 +1739,6 @@
"$ref": "#/definitions/ShellScopeEntryAllowedArgs" "$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 "additionalProperties": false
@@ -1750,6 +1750,10 @@
"sidecar" "sidecar"
], ],
"properties": { "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": { "args": {
"description": "The allowed arguments for the command execution.", "description": "The allowed arguments for the command execution.",
"allOf": [ "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": { "sidecar": {
"description": "If this command is a sidecar command.", "description": "If this command is a sidecar command.",
"type": "boolean" "type": "boolean"
@@ -1784,6 +1784,14 @@
"name" "name"
], ],
"properties": { "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": { "args": {
"description": "The allowed arguments for the command execution.", "description": "The allowed arguments for the command execution.",
"allOf": [ "allOf": [
@@ -1791,14 +1799,6 @@
"$ref": "#/definitions/ShellScopeEntryAllowedArgs" "$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 "additionalProperties": false
@@ -1810,6 +1810,10 @@
"sidecar" "sidecar"
], ],
"properties": { "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": { "args": {
"description": "The allowed arguments for the command execution.", "description": "The allowed arguments for the command execution.",
"allOf": [ "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": { "sidecar": {
"description": "If this command is a sidecar command.", "description": "If this command is a sidecar command.",
"type": "boolean" "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": { "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.", "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": [ "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
}
]
} }
} }
} }

View File

@@ -1724,6 +1724,14 @@
"name" "name"
], ],
"properties": { "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": { "args": {
"description": "The allowed arguments for the command execution.", "description": "The allowed arguments for the command execution.",
"allOf": [ "allOf": [
@@ -1731,14 +1739,6 @@
"$ref": "#/definitions/ShellScopeEntryAllowedArgs" "$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 "additionalProperties": false
@@ -1750,6 +1750,10 @@
"sidecar" "sidecar"
], ],
"properties": { "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": { "args": {
"description": "The allowed arguments for the command execution.", "description": "The allowed arguments for the command execution.",
"allOf": [ "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": { "sidecar": {
"description": "If this command is a sidecar command.", "description": "If this command is a sidecar command.",
"type": "boolean" "type": "boolean"
@@ -1784,6 +1784,14 @@
"name" "name"
], ],
"properties": { "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": { "args": {
"description": "The allowed arguments for the command execution.", "description": "The allowed arguments for the command execution.",
"allOf": [ "allOf": [
@@ -1791,14 +1799,6 @@
"$ref": "#/definitions/ShellScopeEntryAllowedArgs" "$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 "additionalProperties": false
@@ -1810,6 +1810,10 @@
"sidecar" "sidecar"
], ],
"properties": { "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": { "args": {
"description": "The allowed arguments for the command execution.", "description": "The allowed arguments for the command execution.",
"allOf": [ "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": { "sidecar": {
"description": "If this command is a sidecar command.", "description": "If this command is a sidecar command.",
"type": "boolean" "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": { "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.", "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": [ "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
}
]
} }
} }
} }

View File

@@ -1,12 +1,12 @@
use std::str::FromStr; use std::str::FromStr;
use crate::utils;
use anyhow::Ok; use anyhow::Ok;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use migrate::MigrateOpts; use migrate::MigrateOpts;
use nyanpasu_egui::widget::StatisticWidgetVariant;
use tauri::utils::platform::current_exe; use tauri::utils::platform::current_exe;
use crate::utils;
mod migrate; mod migrate;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@@ -38,6 +38,8 @@ enum Commands {
}, },
/// Show a panic dialog while the application is enter panic handler. /// Show a panic dialog while the application is enter panic handler.
PanicDialog { message: String }, PanicDialog { message: String },
/// Launch the Widget with the specified name.
StatisticWidget { variant: StatisticWidgetVariant },
} }
struct DelayedExitGuard; struct DelayedExitGuard;
@@ -92,6 +94,10 @@ pub fn parse() -> anyhow::Result<()> {
Commands::PanicDialog { message } => { Commands::PanicDialog { message } => {
crate::utils::dialog::panic_dialog(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); drop(guard);
std::process::exit(0); std::process::exit(0);

View File

@@ -8,9 +8,11 @@ use specta::Type;
mod clash_strategy; mod clash_strategy;
pub mod logging; pub mod logging;
mod widget;
pub use self::clash_strategy::{ClashStrategy, ExternalControllerPortStrategy}; pub use self::clash_strategy::{ClashStrategy, ExternalControllerPortStrategy};
pub use logging::LoggingLevel; pub use logging::LoggingLevel;
pub use widget::NetworkStatisticWidgetConfig;
// TODO: when support sing-box, remove this struct // TODO: when support sing-box, remove this struct
#[bitflags] #[bitflags]
@@ -132,6 +134,7 @@ impl AsRef<str> for TunStack {
/// ### `verge.yaml` schema /// ### `verge.yaml` schema
#[derive(Default, Debug, Clone, Deserialize, Serialize, VergePatch, specta::Type)] #[derive(Default, Debug, Clone, Deserialize, Serialize, VergePatch, specta::Type)]
#[verge(patch_fn = "patch_config")] #[verge(patch_fn = "patch_config")]
// TODO: use new managedState and builder pattern instead
pub struct IVerge { pub struct IVerge {
/// app listening port for app singleton /// app listening port for app singleton
pub app_singleton_port: Option<u16>, pub app_singleton_port: Option<u16>,
@@ -248,6 +251,10 @@ pub struct IVerge {
/// Tun 堆栈选择 /// Tun 堆栈选择
/// TODO: 弃用此字段,转移到 clash config 里 /// TODO: 弃用此字段,转移到 clash config 里
pub tun_stack: Option<TunStack>, 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)] #[derive(Default, Debug, Clone, Deserialize, Serialize, Type)]

View 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),
}

View 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)
}
}

View File

@@ -94,6 +94,7 @@ pub trait ProfileCleanup: ProfileHelper {
#[delegate(ProfileSharedSetter)] #[delegate(ProfileSharedSetter)]
#[delegate(ProfileSharedGetter)] #[delegate(ProfileSharedGetter)]
#[delegate(ProfileFileIo)] #[delegate(ProfileFileIo)]
#[specta(untagged)]
pub enum Profile { pub enum Profile {
Remote(RemoteProfile), Remote(RemoteProfile),
Local(LocalProfile), Local(LocalProfile),

View File

@@ -1,4 +1,6 @@
pub mod builder;
pub mod item; pub mod item;
pub mod item_type; pub mod item_type;
pub mod profiles; pub mod profiles;
pub use builder::ProfileBuilder;
use item::deserialize_single_or_vec; use item::deserialize_single_or_vec;

View File

@@ -1,8 +1,6 @@
use super::{ use super::{
item::{ builder::ProfileBuilder,
prelude::*, LocalProfileBuilder, MergeProfileBuilder, Profile, RemoteProfileBuilder, item::{prelude::*, Profile},
ScriptProfileBuilder,
},
item_type::ProfileUid, item_type::ProfileUid,
}; };
use crate::utils::{dirs, help}; use crate::utils::{dirs, help};
@@ -16,14 +14,6 @@ use serde_yaml::Mapping;
use std::borrow::Borrow; use std::borrow::Borrow;
use tracing_attributes::instrument; 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 /// Define the `profiles.yaml` schema
#[derive(Debug, Clone, Deserialize, Serialize, Builder, BuilderUpdate, specta::Type)] #[derive(Debug, Clone, Deserialize, Serialize, Builder, BuilderUpdate, specta::Type)]
#[builder(derive(Serialize, Deserialize, specta::Type))] #[builder(derive(Serialize, Deserialize, specta::Type))]
@@ -140,7 +130,7 @@ impl Profiles {
/// update the item value /// update the item value
#[instrument] #[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:?}"); tracing::debug!("patch item: {uid} with {patch:?}");
let item = self let item = self
@@ -154,10 +144,10 @@ impl Profiles {
tracing::debug!("patch item: {item:?}"); tracing::debug!("patch item: {item:?}");
match (item, patch) { match (item, patch) {
(Profile::Remote(item), ProfileKind::Remote(builder)) => item.apply(builder), (Profile::Remote(item), ProfileBuilder::Remote(builder)) => item.apply(builder),
(Profile::Local(item), ProfileKind::Local(builder)) => item.apply(builder), (Profile::Local(item), ProfileBuilder::Local(builder)) => item.apply(builder),
(Profile::Merge(item), ProfileKind::Merge(builder)) => item.apply(builder), (Profile::Merge(item), ProfileBuilder::Merge(builder)) => item.apply(builder),
(Profile::Script(item), ProfileKind::Script(builder)) => item.apply(builder), (Profile::Script(item), ProfileBuilder::Script(builder)) => item.apply(builder),
_ => bail!("profile type mismatch when patching"), _ => bail!("profile type mismatch when patching"),
}; };

View File

@@ -49,3 +49,7 @@ static APP_HANDLE: OnceCell<AppHandle> = OnceCell::new();
pub fn app_handle() -> &'static AppHandle { pub fn app_handle() -> &'static AppHandle {
APP_HANDLE.get().expect("app handle not initialized") APP_HANDLE.get().expect("app handle not initialized")
} }
pub(super) fn setup_app_handle(app_handle: AppHandle) {
let _ = APP_HANDLE.set(app_handle);
}

View File

@@ -1,8 +1,13 @@
use backon::ExponentialBuilder; use backon::ExponentialBuilder;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use specta::Type;
use tauri_specta::Event;
pub mod api; pub mod api;
pub mod core; pub mod core;
pub mod proxies; pub mod proxies;
pub mod ws;
pub static CLASH_API_DEFAULT_BACKOFF_STRATEGY: Lazy<ExponentialBuilder> = Lazy::new(|| { pub static CLASH_API_DEFAULT_BACKOFF_STRATEGY: Lazy<ExponentialBuilder> = Lazy::new(|| {
ExponentialBuilder::default() 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_delay(std::time::Duration::from_secs(5))
.with_max_times(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(())
}

View File

@@ -261,7 +261,7 @@ type ProxiesGuardSingleton = &'static Arc<RwLock<ProxiesGuard>>;
impl ProxiesGuardExt for ProxiesGuardSingleton { impl ProxiesGuardExt for ProxiesGuardSingleton {
async fn update(&self) -> Result<()> { async fn update(&self) -> Result<()> {
let proxies = Proxies::fetch().await?; 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 checksum = adler32(buf.as_bytes())?;
{ {
let reader = self.read(); let reader = self.read();

View File

@@ -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();
}
}

View File

@@ -347,5 +347,5 @@ pub async fn status<'a>() -> anyhow::Result<nyanpasu_ipc::types::StatusInfo<'a>>
} }
let mut status = String::from_utf8(output.stdout)?; let mut status = String::from_utf8(output.stdout)?;
tracing::trace!("service status: {}", status); tracing::trace!("service status: {}", status);
Ok(unsafe { simd_json::serde::from_str(&mut status)? }) Ok(serde_json::from_str(&mut status)?)
} }

View File

@@ -101,7 +101,7 @@ where
/// get the current state, it will return the ManagedStateLocker for the state /// get the current state, it will return the ManagedStateLocker for the state
pub fn latest(&self) -> MappedRwLockReadGuard<'_, T> { 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(); let draft = self.draft.read();
RwLockReadGuard::map(draft, |guard| guard.as_ref().unwrap()) RwLockReadGuard::map(draft, |guard| guard.as_ref().unwrap())
} else { } else {
@@ -125,8 +125,8 @@ where
self.is_dirty self.is_dirty
.store(true, std::sync::atomic::Ordering::Release); .store(true, std::sync::atomic::Ordering::Release);
RwLockWriteGuard::map(self.draft.write(), |guard| { RwLockWriteGuard::map(self.draft.write(), move |guard| {
*guard = Some(state.clone()); *guard = Some(state);
guard.as_mut().unwrap() guard.as_mut().unwrap()
}) })
} }

View File

@@ -73,8 +73,7 @@ impl JobExt for EventsRotateJob {
fn setup(&self) -> Option<crate::core::tasks::task::Task> { fn setup(&self) -> Option<crate::core::tasks::task::Task> {
Some(crate::core::tasks::task::Task { Some(crate::core::tasks::task::Task {
name: CLEAR_EVENTS_TASK_NAME.to_string(), name: CLEAR_EVENTS_TASK_NAME.to_string(),
// 12:00 every day schedule: TaskSchedule::Cron("@hourly".to_string()),
schedule: TaskSchedule::Cron("0 12 * * *".to_string()),
executor: TaskExecutor::Async(Box::new(self.clone())), executor: TaskExecutor::Async(Box::new(self.clone())),
..Default::default() ..Default::default()
}) })

View File

@@ -33,8 +33,7 @@ impl TaskStorage {
let value = table.get(Self::TASKS_KEY.as_bytes())?; let value = table.get(Self::TASKS_KEY.as_bytes())?;
match value { match value {
Some(value) => { Some(value) => {
let mut value = value.value().to_owned(); let tasks: Vec<TaskID> = serde_json::from_slice(value.value())?;
let tasks: Vec<TaskID> = simd_json::from_slice(value.as_mut_slice())?;
Ok(tasks) Ok(tasks)
} }
None => Ok(Vec::new()), None => Ok(Vec::new()),
@@ -50,14 +49,12 @@ impl TaskStorage {
let mut tasks = table let mut tasks = table
.get(Self::TASKS_KEY.as_bytes())? .get(Self::TASKS_KEY.as_bytes())?
.and_then(|val| { .and_then(|val| {
let mut value = val.value().to_owned(); let tasks: HashSet<TaskID> = serde_json::from_slice(val.value()).ok()?;
let tasks: HashSet<TaskID> =
simd_json::from_slice(value.as_mut_slice()).ok()?;
Some(tasks) Some(tasks)
}) })
.unwrap_or_default(); .unwrap_or_default();
tasks.insert(task_id); 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())?; table.insert(Self::TASKS_KEY.as_bytes(), value.as_slice())?;
} }
write_txn.commit()?; write_txn.commit()?;
@@ -85,8 +82,7 @@ impl TaskStorage {
let value = table.get(key.as_bytes())?; let value = table.get(key.as_bytes())?;
match value { match value {
Some(value) => { Some(value) => {
let mut value = value.value().to_owned(); let event: TaskEvent = serde_json::from_slice(value.value())?;
let event: TaskEvent = simd_json::from_slice(value.as_mut_slice())?;
Ok(Some(event)) Ok(Some(event))
} }
None => Ok(None), None => Ok(None),
@@ -116,10 +112,7 @@ impl TaskStorage {
let table = read_txn.open_table(NYANPASU_TABLE)?; let table = read_txn.open_table(NYANPASU_TABLE)?;
let value = table.get(key.as_bytes())?; let value = table.get(key.as_bytes())?;
let value: Vec<TaskEventID> = match value { let value: Vec<TaskEventID> = match value {
Some(value) => { Some(value) => serde_json::from_slice(value.value())?,
let mut value = value.value().to_owned();
simd_json::from_slice(value.as_mut_slice())?
}
None => return Ok(None), None => return Ok(None),
}; };
Ok(Some(value)) Ok(Some(value))
@@ -133,8 +126,8 @@ impl TaskStorage {
let db = self.storage.get_instance(); let db = self.storage.get_instance();
let event_key = format!("task:event:id:{}", event.id); let event_key = format!("task:event:id:{}", event.id);
let event_ids_key = format!("task:events:task_id:{}", event.task_id); let event_ids_key = format!("task:events:task_id:{}", event.task_id);
let event_value = simd_json::to_vec(event)?; let event_value = serde_json::to_vec(event)?;
let event_ids = simd_json::to_vec(&event_ids)?; let event_ids = serde_json::to_vec(&event_ids)?;
let write_txn = db.begin_write()?; let write_txn = db.begin_write()?;
{ {
let mut table = write_txn.open_table(NYANPASU_TABLE)?; let mut table = write_txn.open_table(NYANPASU_TABLE)?;
@@ -149,7 +142,7 @@ impl TaskStorage {
pub fn update_event(&self, event: &TaskEvent) -> Result<()> { pub fn update_event(&self, event: &TaskEvent) -> Result<()> {
let db = self.storage.get_instance(); let db = self.storage.get_instance();
let event_key = format!("task:event:id:{}", event.id); 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 write_txn = db.begin_write()?;
{ {
let mut table = write_txn.open_table(NYANPASU_TABLE)?; let mut table = write_txn.open_table(NYANPASU_TABLE)?;
@@ -176,7 +169,7 @@ impl TaskStorage {
if event_ids.is_empty() { if event_ids.is_empty() {
table.remove(event_ids_key.as_bytes())?; table.remove(event_ids_key.as_bytes())?;
} else { } 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())?; table.insert(event_ids_key.as_bytes(), event_ids.as_slice())?;
} }
} }
@@ -211,7 +204,7 @@ impl TaskGuard for TaskManager {
let key = key.value(); let key = key.value();
let mut value = value.value().to_owned(); let mut value = value.value().to_owned();
if key.starts_with(b"task:id:") { 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!( debug!(
"restore task: {:?} {:?}", "restore task: {:?} {:?}",
str::from_utf8(key).unwrap(), str::from_utf8(key).unwrap(),
@@ -234,7 +227,7 @@ impl TaskGuard for TaskManager {
let mut table = write_txn.open_table(NYANPASU_TABLE)?; let mut table = write_txn.open_table(NYANPASU_TABLE)?;
for task in tasks { for task in tasks {
let key = format!("task:id:{}", task.id); 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())?; table.insert(key.as_bytes(), value.as_slice())?;
} }
} }

View File

@@ -31,7 +31,7 @@ pub enum Error {
DatabaseCommitOperationFailed(#[from] redb::CommitError), DatabaseCommitOperationFailed(#[from] redb::CommitError),
#[error("json parse failed: {0:?}")] #[error("json parse failed: {0:?}")]
JsonParseFailed(#[from] simd_json::Error), JsonParseFailed(#[from] serde_json::Error),
#[error("task issue failed: {message:?}")] #[error("task issue failed: {message:?}")]
InnerTask { InnerTask {

View File

@@ -182,11 +182,11 @@ impl Runner for JSRunner {
let boa_runner = wrap_result!(BoaRunner::try_new(), take_logs(logs)); let boa_runner = wrap_result!(BoaRunner::try_new(), take_logs(logs));
wrap_result!(boa_runner.setup_console(logger), take_logs(logs)); wrap_result!(boa_runner.setup_console(logger), take_logs(logs));
let config = wrap_result!( 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) }), .map_err(|e| { std::io::Error::new(std::io::ErrorKind::InvalidData, e) }),
take_logs(logs) 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!( let execute_module = format!(
r#"import process from "./{hash}.mjs"; r#"import process from "./{hash}.mjs";
let config = JSON.parse({config}); let config = JSON.parse({config});
@@ -220,7 +220,7 @@ impl Runner for JSRunner {
take_logs(logs) take_logs(logs)
); );
let mapping = wrap_result!( 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) }), .map_err(|e| { std::io::Error::new(std::io::ErrorKind::InvalidData, e) }),
take_logs(logs) take_logs(logs)
); );
@@ -451,7 +451,7 @@ const foreignNameservers = [
"Test".to_string() "Test".to_string()
),]) ),])
); );
let outs = simd_json::serde::to_string(&logs).unwrap(); let outs = serde_json::to_string(&logs).unwrap();
assert_eq!( assert_eq!(
outs, outs,
r#"[["log","Test console log"],["warn","Test console log"],["error","Test console log"]]"# 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()) 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#"[]"#); assert_eq!(outs, r#"[]"#);
}); });
} }

View 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,
{
}

View 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(())
}

View File

@@ -7,16 +7,17 @@
use std::borrow::Borrow; use std::borrow::Borrow;
use crate::{ use crate::{
config::*, config::{nyanpasu::NetworkStatisticWidgetConfig, *},
core::{service::ipc::get_ipc_state, *}, core::{service::ipc::get_ipc_state, *},
log_err, log_err,
utils::{self, help::get_clash_external_port, resolve}, utils::{self, help::get_clash_external_port, resolve},
}; };
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use handle::Message; use handle::Message;
use nyanpasu_egui::widget::network_statistic_large;
use nyanpasu_ipc::api::status::CoreState; use nyanpasu_ipc::api::status::CoreState;
use serde_yaml::{Mapping, Value}; use serde_yaml::{Mapping, Value};
use tauri::AppHandle; use tauri::{AppHandle, Manager};
use tauri_plugin_clipboard_manager::ClipboardExt; 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_level = patch.app_log_level;
let log_max_files = patch.max_log_files; let log_max_files = patch.max_log_files;
let enable_tray_selector = patch.clash_tray_selector; let enable_tray_selector = patch.clash_tray_selector;
let network_statistic_widget = patch.network_statistic_widget;
let res = || async move { let res = || async move {
let service_mode = patch.enable_service_mode; let service_mode = patch.enable_service_mode;
let ipc_state = get_ipc_state(); let ipc_state = get_ipc_state();
@@ -362,6 +363,24 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
handle::Handle::update_systray()?; 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(()) <Result<()>>::Ok(())
}; };

View File

@@ -1,5 +1,5 @@
use crate::{ use crate::{
config::*, config::{profile::ProfileBuilder, *},
core::{storage::Storage, tasks::jobs::ProfilesJobGuard, updater::ManifestVersionLatest, *}, core::{storage::Storage, tasks::jobs::ProfilesJobGuard, updater::ManifestVersionLatest, *},
enhance::PostProcessingOutput, enhance::PostProcessingOutput,
feat, feat,
@@ -147,26 +147,26 @@ pub async fn import_profile(url: String, option: Option<RemoteProfileOptionsBuil
/// create a new profile /// create a new profile
#[tauri::command] #[tauri::command]
#[specta::specta] #[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:?}"); tracing::trace!("create profile: {item:?}");
let is_remote = matches!(&item, ProfileKind::Remote(_)); let is_remote = matches!(&item, ProfileBuilder::Remote(_));
let profile: Profile = match item { let profile: Profile = match item {
ProfileKind::Local(builder) => builder ProfileBuilder::Local(builder) => builder
.build() .build()
.context("failed to build local profile")? .context("failed to build local profile")?
.into(), .into(),
ProfileKind::Remote(mut builder) => builder ProfileBuilder::Remote(mut builder) => builder
.build_no_blocking() .build_no_blocking()
.await .await
.context("failed to build remote profile")? .context("failed to build remote profile")?
.into(), .into(),
ProfileKind::Merge(builder) => builder ProfileBuilder::Merge(builder) => builder
.build() .build()
.context("failed to build merge profile")? .context("failed to build merge profile")?
.into(), .into(),
ProfileKind::Script(builder) => builder ProfileBuilder::Script(builder) => builder
.build() .build()
.context("failed to build script profile")? .context("failed to build script profile")?
.into(), .into(),
@@ -277,7 +277,7 @@ pub async fn patch_profiles_config(profiles: ProfilesBuilder) -> Result {
/// update profile by uid /// update profile by uid
#[tauri::command] #[tauri::command]
#[specta::specta] #[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:?}"); tracing::debug!("patch profile: {uid} with {profile:?}");
{ {
let committer = Config::profiles().auto_commit(); let committer = Config::profiles().auto_commit();
@@ -419,9 +419,6 @@ pub fn get_postprocessing_output() -> Result<PostProcessingOutput> {
Ok(Config::runtime().latest().postprocessing_output.clone()) Ok(Config::runtime().latest().postprocessing_output.clone())
} }
#[derive(specta::Type)]
pub struct Test<'n>(Cow<'n, CoreState>, i64, RunType);
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn get_core_status<'n>() -> Result<(Cow<'n, CoreState>, i64, RunType)> { 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))?; (storage.remove_item(&key))?;
Ok(()) 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())
}

View File

@@ -10,11 +10,13 @@ mod config;
mod consts; mod consts;
mod core; mod core;
mod enhance; mod enhance;
mod event_handler;
mod feat; mod feat;
mod ipc; mod ipc;
mod server; mod server;
mod setup; mod setup;
mod utils; mod utils;
mod widget;
mod window; mod window;
use std::io; use std::io;
@@ -26,7 +28,7 @@ use crate::{
}; };
use specta_typescript::{BigIntExportBehavior, Typescript}; use specta_typescript::{BigIntExportBehavior, Typescript};
use tauri::{Emitter, Manager}; 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}; use utils::resolve::{is_window_opened, reset_window_open_counter};
rust_i18n::i18n!("../../locales"); rust_i18n::i18n!("../../locales");
@@ -176,84 +178,88 @@ pub fn run() -> std::io::Result<()> {
})); }));
// setup specta // setup specta
let specta_builder = tauri_specta::Builder::<tauri::Wry>::new().commands(collect_commands![ let specta_builder = tauri_specta::Builder::<tauri::Wry>::new()
// common .commands(collect_commands![
ipc::get_sys_proxy, // common
ipc::open_app_config_dir, ipc::get_sys_proxy,
ipc::open_app_data_dir, ipc::open_app_config_dir,
ipc::open_logs_dir, ipc::open_app_data_dir,
ipc::open_web_url, ipc::open_logs_dir,
ipc::open_core_dir, ipc::open_web_url,
// cmds::kill_sidecar, ipc::open_core_dir,
ipc::restart_sidecar, // cmds::kill_sidecar,
// clash ipc::restart_sidecar,
ipc::get_clash_info, // clash
ipc::get_clash_logs, ipc::get_clash_info,
ipc::patch_clash_config, ipc::get_clash_logs,
ipc::change_clash_core, ipc::patch_clash_config,
ipc::get_runtime_config, ipc::change_clash_core,
ipc::get_runtime_yaml, ipc::get_runtime_config,
ipc::get_runtime_exists, ipc::get_runtime_yaml,
ipc::get_postprocessing_output, ipc::get_runtime_exists,
ipc::clash_api_get_proxy_delay, ipc::get_postprocessing_output,
ipc::uwp::invoke_uwp_tool, ipc::clash_api_get_proxy_delay,
// updater ipc::uwp::invoke_uwp_tool,
ipc::fetch_latest_core_versions, // updater
ipc::update_core, ipc::fetch_latest_core_versions,
ipc::inspect_updater, ipc::update_core,
ipc::get_core_version, ipc::inspect_updater,
// utils ipc::get_core_version,
ipc::collect_logs, // utils
// verge ipc::collect_logs,
ipc::get_verge_config, // verge
ipc::patch_verge_config, ipc::get_verge_config,
// cmds::update_hotkeys, ipc::patch_verge_config,
// profile // cmds::update_hotkeys,
ipc::get_profiles, // profile
ipc::enhance_profiles, ipc::get_profiles,
ipc::patch_profiles_config, ipc::enhance_profiles,
ipc::view_profile, ipc::patch_profiles_config,
ipc::patch_profile, ipc::view_profile,
ipc::create_profile, ipc::patch_profile,
ipc::import_profile, ipc::create_profile,
ipc::reorder_profile, ipc::import_profile,
ipc::reorder_profiles_by_list, ipc::reorder_profile,
ipc::update_profile, ipc::reorder_profiles_by_list,
ipc::delete_profile, ipc::update_profile,
ipc::read_profile_file, ipc::delete_profile,
ipc::save_profile_file, ipc::read_profile_file,
ipc::save_window_size_state, ipc::save_profile_file,
ipc::get_custom_app_dir, ipc::save_window_size_state,
ipc::set_custom_app_dir, ipc::get_custom_app_dir,
// service mode ipc::set_custom_app_dir,
ipc::service::status_service, // service mode
ipc::service::install_service, ipc::service::status_service,
ipc::service::uninstall_service, ipc::service::install_service,
ipc::service::start_service, ipc::service::uninstall_service,
ipc::service::stop_service, ipc::service::start_service,
ipc::service::restart_service, ipc::service::stop_service,
ipc::is_portable, ipc::service::restart_service,
ipc::get_proxies, ipc::is_portable,
ipc::select_proxy, ipc::get_proxies,
ipc::update_proxy_provider, ipc::select_proxy,
ipc::restart_application, ipc::update_proxy_provider,
ipc::collect_envs, ipc::restart_application,
ipc::get_server_port, ipc::collect_envs,
ipc::set_tray_icon, ipc::get_server_port,
ipc::is_tray_icon_set, ipc::set_tray_icon,
ipc::get_core_status, ipc::is_tray_icon_set,
ipc::url_delay_test, ipc::get_core_status,
ipc::get_ipsb_asn, ipc::url_delay_test,
ipc::open_that, ipc::get_ipsb_asn,
ipc::is_appimage, ipc::open_that,
ipc::get_service_install_prompt, ipc::is_appimage,
ipc::cleanup_processes, ipc::get_service_install_prompt,
ipc::get_storage_item, ipc::cleanup_processes,
ipc::set_storage_item, ipc::get_storage_item,
ipc::remove_storage_item, ipc::set_storage_item,
ipc::mutate_proxies, ipc::remove_storage_item,
ipc::get_core_dir, ipc::mutate_proxies,
]); ipc::get_core_dir,
// clash layer
ipc::get_clash_ws_connections_state,
])
.events(collect_events![core::clash::ClashConnectionsEvent]);
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
{ {
@@ -310,7 +316,9 @@ pub fn run() -> std::io::Result<()> {
.plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_global_shortcut::Builder::default().build()) .plugin(tauri_plugin_global_shortcut::Builder::default().build())
.setup(|app| { .setup(move |app| {
specta_builder.mount_events(app);
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
use tauri::menu::{MenuBuilder, SubmenuBuilder}; use tauri::menu::{MenuBuilder, SubmenuBuilder};

View File

@@ -108,6 +108,7 @@ pub fn resolve_setup(app: &mut App) {
}); });
handle::Handle::global().init(app.app_handle().clone()); 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_resources());
log_err!(init::init_service()); log_err!(init::init_service());
@@ -150,6 +151,19 @@ pub fn resolve_setup(app: &mut App) {
log::trace!("launch core"); log::trace!("launch core");
log_err!(CoreManager::global().init()); 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"); log::trace!("init system tray");
#[cfg(any(windows, target_os = "linux"))] #[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 tray::icon::resize_images(crate::utils::help::get_max_scale_factor()); // generate latest cache icon by current scale factor

View 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(())
}

View File

@@ -11,6 +11,7 @@
"build": "tsc" "build": "tsc"
}, },
"dependencies": { "dependencies": {
"@tanstack/react-query": "5.66.0",
"@tauri-apps/api": "2.2.0", "@tauri-apps/api": "2.2.0",
"ahooks": "3.8.4", "ahooks": "3.8.4",
"ofetch": "1.4.1", "ofetch": "1.4.1",

View File

@@ -1,3 +1,5 @@
export * from './ipc' export * from './ipc'
export * from './service'
export * from './openapi' export * from './openapi'
export * from './provider'
export * from './service'
export * from './utils'

View File

@@ -304,7 +304,7 @@ export const commands = {
*/ */
async patchProfile( async patchProfile(
uid: string, uid: string,
profile: ProfileKind, profile: ProfileBuilder,
): Promise<Result<null, string>> { ): Promise<Result<null, string>> {
try { try {
return { return {
@@ -320,7 +320,7 @@ export const commands = {
* create a new profile * create a new profile
*/ */
async createProfile( async createProfile(
item: ProfileKind, item: ProfileBuilder,
fileData: string | null, fileData: string | null,
): Promise<Result<null, string>> { ): Promise<Result<null, string>> {
try { try {
@@ -707,10 +707,29 @@ export const commands = {
else return { status: 'error', error: e as any } 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 **/ /** user-defined events **/
export const events = __makeEvents__<{
clashConnectionsEvent: ClashConnectionsEvent
}>({
clashConnectionsEvent: 'clash-connections-event',
})
/** user-defined constants **/ /** user-defined constants **/
/** user-defined types **/ /** user-defined types **/
@@ -736,6 +755,20 @@ export type ChunkStatus = {
speed: number speed: number
} }
export type ChunkThreadState = 'Idle' | 'Downloading' | 'Finished' 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 = export type ClashCore =
| 'clash' | 'clash'
| 'clash-rs' | 'clash-rs'
@@ -964,6 +997,10 @@ export type IVerge = {
* TODO: 弃用此字段,转移到 clash config 里 * TODO: 弃用此字段,转移到 clash config 里
*/ */
tun_stack: TunStack | null tun_stack: TunStack | null
/**
* 是否启用网络统计信息浮窗
*/
network_statistic_widget?: NetworkStatisticWidgetConfig | null
} }
export type IVergeTheme = { export type IVergeTheme = {
primary_color: string | null primary_color: string | null
@@ -1122,6 +1159,9 @@ export type MergeProfileBuilder = {
*/ */
updated: number | null updated: number | null
} }
export type NetworkStatisticWidgetConfig =
| { kind: 'disabled' }
| { kind: 'enabled'; value: StatisticWidgetVariant }
export type PatchRuntimeConfig = { export type PatchRuntimeConfig = {
allow_lan?: boolean | null allow_lan?: boolean | null
ipv6?: boolean | null ipv6?: boolean | null
@@ -1148,20 +1188,20 @@ export type PostProcessingOutput = {
advice: [LogSpan, string][] advice: [LogSpan, string][]
} }
export type Profile = export type Profile =
| { Remote: RemoteProfile } | RemoteProfile
| { Local: LocalProfile } | LocalProfile
| { Merge: MergeProfile } | MergeProfile
| { Script: ScriptProfile } | ScriptProfile
export type ProfileBuilder =
| RemoteProfileBuilder
| LocalProfileBuilder
| MergeProfileBuilder
| ScriptProfileBuilder
export type ProfileItemType = export type ProfileItemType =
| 'remote' | 'remote'
| 'local' | 'local'
| { script: ScriptType } | { script: ScriptType }
| 'merge' | 'merge'
export type ProfileKind =
| { Remote: RemoteProfileBuilder }
| { Local: LocalProfileBuilder }
| { Merge: MergeProfileBuilder }
| { Script: ScriptProfileBuilder }
/** /**
* Define the `profiles.yaml` schema * Define the `profiles.yaml` schema
*/ */
@@ -1443,6 +1483,7 @@ export type ScriptProfileBuilder = {
} }
export type ScriptType = 'javascript' | 'lua' export type ScriptType = 'javascript' | 'lua'
export type ServiceStatus = 'not_installed' | 'stopped' | 'running' export type ServiceStatus = 'not_installed' | 'stopped' | 'running'
export type StatisticWidgetVariant = 'large' | 'small'
export type StatusInfo = { export type StatusInfo = {
name: string name: string
version: string version: string

View File

@@ -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,
}
}

View 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,
}
}

View 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>
)
}

View 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
}

View File

@@ -30,7 +30,7 @@
"dayjs": "1.11.13", "dayjs": "1.11.13",
"framer-motion": "12.0.6", "framer-motion": "12.0.6",
"i18next": "24.2.2", "i18next": "24.2.2",
"jotai": "2.11.1", "jotai": "2.11.3",
"json-schema": "0.4.0", "json-schema": "0.4.0",
"material-react-table": "3.1.0", "material-react-table": "3.1.0",
"monaco-editor": "0.52.2", "monaco-editor": "0.52.2",
@@ -45,19 +45,20 @@
"react-split-grid": "1.0.4", "react-split-grid": "1.0.4",
"react-use": "17.6.0", "react-use": "17.6.0",
"swr": "2.3.0", "swr": "2.3.0",
"virtua": "0.39.3", "virtua": "0.40.0",
"vite-bundle-visualizer": "1.2.1" "vite-bundle-visualizer": "1.2.1"
}, },
"devDependencies": { "devDependencies": {
"@csstools/normalize.css": "12.1.1", "@csstools/normalize.css": "12.1.1",
"@emotion/babel-plugin": "11.13.5", "@emotion/babel-plugin": "11.13.5",
"@emotion/react": "11.14.0", "@emotion/react": "11.14.0",
"@iconify/json": "2.2.301", "@iconify/json": "2.2.302",
"@monaco-editor/react": "4.6.0", "@monaco-editor/react": "4.6.0",
"@tanstack/react-query": "5.64.2", "@tailwindcss/vite": "4.0.3",
"@tanstack/react-router": "1.97.17", "@tanstack/react-query": "5.66.0",
"@tanstack/router-devtools": "1.97.17", "@tanstack/react-router": "1.99.0",
"@tanstack/router-plugin": "1.97.17", "@tanstack/router-devtools": "1.99.0",
"@tanstack/router-plugin": "1.99.0",
"@tauri-apps/plugin-clipboard-manager": "2.2.1", "@tauri-apps/plugin-clipboard-manager": "2.2.1",
"@tauri-apps/plugin-dialog": "2.2.0", "@tauri-apps/plugin-dialog": "2.2.0",
"@tauri-apps/plugin-fs": "2.2.0", "@tauri-apps/plugin-fs": "2.2.0",
@@ -65,7 +66,7 @@
"@tauri-apps/plugin-os": "2.2.0", "@tauri-apps/plugin-os": "2.2.0",
"@tauri-apps/plugin-process": "2.2.0", "@tauri-apps/plugin-process": "2.2.0",
"@tauri-apps/plugin-shell": "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": "19.0.8",
"@types/react-dom": "19.0.3", "@types/react-dom": "19.0.3",
"@types/validator": "13.12.2", "@types/validator": "13.12.2",
@@ -80,7 +81,7 @@
"monaco-yaml": "5.2.3", "monaco-yaml": "5.2.3",
"nanoid": "5.0.9", "nanoid": "5.0.9",
"sass-embedded": "1.83.4", "sass-embedded": "1.83.4",
"shiki": "1.29.2", "shiki": "2.2.0",
"tailwindcss-textshadow": "2.1.3", "tailwindcss-textshadow": "2.1.3",
"unplugin-auto-import": "19.0.0", "unplugin-auto-import": "19.0.0",
"unplugin-icons": "22.0.0", "unplugin-icons": "22.0.0",

View File

@@ -4,6 +4,5 @@ export default {
'postcss-import': {}, 'postcss-import': {},
'postcss-html': {}, 'postcss-html': {},
autoprefixer: {}, autoprefixer: {},
tailwindcss: {},
}, },
} }

View File

@@ -1,3 +1,5 @@
@tailwind base; /* stylelint-disable import-notation */
@tailwind components;
@import 'tailwindcss';
@tailwind utilities; @tailwind utilities;

View File

@@ -74,12 +74,12 @@ export const AppContainer = ({
<div className={styles.container}> <div className={styles.container}>
{OS === 'windows' && ( {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 */} {/* TODO: add a framer motion animation to toggle the maximized state */}
{OS === 'macos' && !isMaximized && ( {OS === 'macos' && !isMaximized && (
<div <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) }} style={{ backgroundColor: alpha(palette.primary.main, 0.1) }}
/> />
)} )}

View File

@@ -17,7 +17,7 @@ export const AppDrawer = () => {
<div <div
className={cn( className={cn(
'fixed z-10 flex items-center gap-2', '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 data-tauri-drag-region
> >

View File

@@ -40,7 +40,7 @@ export const DrawerContent = ({
{!onlyIcon && ( {!onlyIcon && (
<div <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 data-tauri-drag-region
> >
{'Clash\nNyanpasu'} {'Clash\nNyanpasu'}
@@ -48,7 +48,7 @@ export const DrawerContent = ({
)} )}
</div> </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 }]) => { {Object.entries(routes).map(([name, { path, icon }]) => {
return ( return (
<RouteListItem <RouteListItem

View File

@@ -73,7 +73,7 @@ export const RouteListItem = ({
{!onlyIcon && ( {!onlyIcon && (
<div <div
className={cn( className={cn(
'w-full text-nowrap pb-1 pt-1', 'w-full pt-1 pb-1 text-nowrap',
nyanpasuConfig?.language && nyanpasuConfig?.language &&
languageQuirks[nyanpasuConfig?.language].drawer.itemClassNames, languageQuirks[nyanpasuConfig?.language].drawer.itemClassNames,
)} )}

View File

@@ -49,7 +49,7 @@ export const IPASNPanel = ({ refreshCount }: { refreshCount: number }) => {
xl: 3, 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 ? (
<> <>
{data.country_code && ( {data.country_code && (
@@ -102,7 +102,7 @@ export const IPASNPanel = ({ refreshCount }: { refreshCount: number }) => {
<span <span
className={cn( 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', 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="flex flex-1 animate-pulse flex-col gap-1">
<div className="mt-1.5 h-6 w-20 rounded-full bg-slate-700" /> <div className="mt-1.5 h-6 w-20 rounded-full bg-slate-700" />

View File

@@ -22,7 +22,7 @@ export const LogItem = ({ value }: { value: LogMessage }) => {
}, [value.payload]) }, [value.payload])
return ( 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"> <div className="flex gap-2">
<span className="font-thin">{value.time}</span> <span className="font-thin">{value.time}</span>
@@ -36,7 +36,7 @@ export const LogItem = ({ value }: { value: LogMessage }) => {
</span> </span>
</div> </div>
<div className="text-wrap border-b border-slate-200 pb-2"> <div className="border-b border-slate-200 pb-2 text-wrap">
<p <p
className={cn( className={cn(
styles.item, styles.item,

View File

@@ -88,7 +88,7 @@ export const ChainItem = memo(function ChainItem({
}} }}
> >
<ListItemButton <ListItemButton
className="!mb-2 !mt-2 !flex !justify-between gap-2" className="!mt-2 !mb-2 !flex !justify-between gap-2"
sx={[ sx={[
{ {
borderRadius: 4, borderRadius: 4,

View File

@@ -68,7 +68,7 @@ export const SideChain = ({ onChainEdit }: SideChainProps) => {
) )
return ( return (
<div className="h-full overflow-auto !pl-2 !pr-2"> <div className="h-full overflow-auto !pr-2 !pl-2">
<Reorder.Group <Reorder.Group
axis="y" axis="y"
values={reorderValues} values={reorderValues}
@@ -97,7 +97,7 @@ export const SideChain = ({ onChainEdit }: SideChainProps) => {
</Reorder.Group> </Reorder.Group>
<ListItemButton <ListItemButton
className="!mb-2 !mt-2 flex justify-center gap-2" className="!mt-2 !mb-2 flex justify-center gap-2"
sx={{ sx={{
backgroundColor: alpha(palette.secondary.main, 0.1), backgroundColor: alpha(palette.secondary.main, 0.1),
borderRadius: 4, borderRadius: 4,

View File

@@ -21,7 +21,7 @@ const LogListItem = memo(function LogListItem({
<> <>
{showDivider && <Divider />} {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="rounded-sm bg-blue-600 px-0.5">{name}</span>
<span className="text-red-500"> [{item?.[0]}]: </span> <span className="text-red-500"> [{item?.[0]}]: </span>
<span>{item?.[1]}</span> <span>{item?.[1]}</span>
@@ -53,7 +53,7 @@ export const SideLog = ({ className }: SideLogProps) => {
<Divider /> <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) ? ( {!isEmpty(getRuntimeLogs.data) ? (
Object.entries(getRuntimeLogs.data).map(([uid, content]) => { Object.entries(getRuntimeLogs.data).map(([uid, content]) => {
return content.map((item, index) => { return content.map((item, index) => {

View File

@@ -170,7 +170,7 @@ export const ProfileDialog = ({
const MetaInfo = useMemo( 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 && ( {!isEdit && (
<SelectElement <SelectElement
label={t('Type')} label={t('Type')}

View File

@@ -234,7 +234,7 @@ export const ProfileItem = memo(function ProfileItem({
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<Tooltip title={item.url}> <Tooltip title={item.url}>
<Chip <Chip
className="!pl-2 !pr-2 font-bold" className="!pr-2 !pl-2 font-bold"
avatar={<IconComponent className="!size-5" color="primary" />} avatar={<IconComponent className="!size-5" color="primary" />}
label={isRemote ? t('Remote') : t('Local')} label={isRemote ? t('Remote') : t('Local')}
/> />
@@ -250,7 +250,7 @@ export const ProfileItem = memo(function ProfileItem({
)} )}
<TextCarousel <TextCarousel
className="w-30 flex h-6 items-center" className="flex h-6 w-30 items-center"
nodes={[ nodes={[
!!item.updated && ( !!item.updated && (
<TimeSpan ts={item.updated!} k="Subscription Updated At" /> <TimeSpan ts={item.updated!} k="Subscription Updated At" />
@@ -348,7 +348,7 @@ export const ProfileItem = memo(function ProfileItem({
<motion.div <motion.div
className={cn( 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', 'flex-col items-center justify-center gap-4',
'text-shadow-xl rounded-3xl font-bold backdrop-blur', 'text-shadow-xl rounded-3xl font-bold backdrop-blur',
)} )}
@@ -379,7 +379,7 @@ function TimeSpan({ ts, k }: { ts: number; k: string }) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Tooltip title={time.format('YYYY/MM/DD HH:mm:ss')}> <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, { {t(k, {
time: time.fromNow(), time: time.fromNow(),
})} })}

View File

@@ -192,8 +192,8 @@ export const ScriptDialog = ({
{...props} {...props}
> >
<div className="flex h-full"> <div className="flex h-full">
<div className="overflow-auto pb-4 pt-4"> <div className="overflow-auto pt-4 pb-4">
<div className="flex flex-col gap-4 pb-4 pl-4 pr-4"> <div className="flex flex-col gap-4 pr-4 pb-4 pl-4">
{!isEdit && ( {!isEdit && (
<SelectElement <SelectElement
label={t('Type')} label={t('Type')}

View File

@@ -47,7 +47,7 @@ export const DelayButton = memo(function DelayButton({
return ( return (
<Tooltip title={t('Delay check')}> <Tooltip title={t('Delay check')}>
<Button <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={{ sx={{
boxShadow: 8, boxShadow: 8,
backgroundColor: alpha( backgroundColor: alpha(

View File

@@ -37,7 +37,7 @@ const RuleItem = ({ index, value }: Props) => {
} }
return ( 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"> <div style={{ color: palette.text.secondary }} className="min-w-14">
{index + 1} {index + 1}
</div> </div>

View File

@@ -71,7 +71,7 @@ const CardProgress = ({
return ( return (
<motion.div <motion.div
className={cn( 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', 'flex flex-col items-center justify-center gap-2',
)} )}
style={{ style={{

View File

@@ -44,7 +44,7 @@ export default function HotkeyInput({
<div className={cn('relative min-h-[36px] w-[165px]', styles.wrapper)}> <div className={cn('relative min-h-[36px] w-[165px]', styles.wrapper)}>
<input <input
className={cn( 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, styles.input,
className, className,
)} )}

View File

@@ -37,7 +37,7 @@ function CopyToClipboardButton({ onClick }: CopyToClipboardButtonProps) {
> >
<IconButton <IconButton
size="small" size="small"
className="absolute right-1 top-1" className="absolute top-1 right-1"
onClick={onClick} onClick={onClick}
> >
<ContentPasteIcon fontSize="small" color="primary" /> <ContentPasteIcon fontSize="small" color="primary" />

View File

@@ -112,7 +112,7 @@ export const SettingNyanpasuVersion = () => {
{isPlatformSupported && ( {isPlatformSupported && (
<> <>
<div className="mb-1 mt-1"> <div className="mt-1 mb-1">
<AutoCheckUpdate /> <AutoCheckUpdate />
</div> </div>
<ListItem sx={{ pl: 0, pr: 0 }}> <ListItem sx={{ pl: 0, pr: 0 }}>

View File

@@ -235,9 +235,9 @@ function ProfilePage() {
</AnimatePresence> </AnimatePresence>
<AddProfileContext.Provider value={addProfileCtxValue}> <AddProfileContext.Provider value={addProfileCtxValue}>
<div className="fixed bottom-8 right-8"> <div className="fixed right-8 bottom-8">
<FloatingButton <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={[ sx={[
(theme) => ({ (theme) => ({
backgroundColor: theme.palette.grey[200], backgroundColor: theme.palette.grey[200],

View File

@@ -114,7 +114,7 @@ function ProxyPage() {
onClick={() => handleSwitch(key)} onClick={() => handleSwitch(key)}
sx={{ textTransform: 'capitalize' }} 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)} {t(key)}
</Button> </Button>
))} ))}

View File

@@ -1,5 +1,6 @@
import type { Highlighter } from 'shiki' import type { Highlighter } from 'shiki'
import { getSingletonHighlighterCore } from 'shiki/core' import { getSingletonHighlighterCore } from 'shiki/core'
import { createOnigurumaEngine } from 'shiki/engine/oniguruma'
import minLight from 'shiki/themes/min-light.mjs' import minLight from 'shiki/themes/min-light.mjs'
import nord from 'shiki/themes/nord.mjs' import nord from 'shiki/themes/nord.mjs'
import getWasm from 'shiki/wasm' import getWasm from 'shiki/wasm'
@@ -9,6 +10,7 @@ let shiki: Highlighter | null = null
export async function getShikiSingleton() { export async function getShikiSingleton() {
if (!shiki) { if (!shiki) {
shiki = (await getSingletonHighlighterCore({ shiki = (await getSingletonHighlighterCore({
engine: createOnigurumaEngine(import('shiki/wasm')),
themes: [nord, minLight], themes: [nord, minLight],
langs: [() => import('shiki/langs/shell.mjs')], langs: [() => import('shiki/langs/shell.mjs')],
loadWasm: getWasm, loadWasm: getWasm,

View File

@@ -8,6 +8,7 @@ import { createHtmlPlugin } from 'vite-plugin-html'
import sassDts from 'vite-plugin-sass-dts' import sassDts from 'vite-plugin-sass-dts'
import svgr from 'vite-plugin-svgr' import svgr from 'vite-plugin-svgr'
import tsconfigPaths from 'vite-tsconfig-paths' import tsconfigPaths from 'vite-tsconfig-paths'
import tailwindPlugin from '@tailwindcss/vite'
// import react from "@vitejs/plugin-react"; // import react from "@vitejs/plugin-react";
import { TanStackRouterVite } from '@tanstack/router-plugin/vite' import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
import legacy from '@vitejs/plugin-legacy' import legacy from '@vitejs/plugin-legacy'
@@ -56,6 +57,7 @@ export default defineConfig(({ command, mode }) => {
}, },
}, },
plugins: [ plugins: [
tailwindPlugin(),
tsconfigPaths(), tsconfigPaths(),
legacy({ legacy({
renderLegacyChunks: false, renderLegacyChunks: false,

View File

@@ -43,7 +43,7 @@
"clsx": "2.1.1", "clsx": "2.1.1",
"d3-interpolate-path": "2.3.0", "d3-interpolate-path": "2.3.0",
"sass-embedded": "1.83.4", "sass-embedded": "1.83.4",
"tailwind-merge": "2.6.0", "tailwind-merge": "3.0.1",
"typescript-plugin-css-modules": "5.1.0", "typescript-plugin-css-modules": "5.1.0",
"vite-plugin-dts": "4.5.0" "vite-plugin-dts": "4.5.0"
} }

View File

@@ -8,6 +8,6 @@
justify-content: center; justify-content: center;
width: 100%; width: 100%;
height: 100%; height: 100%;
backdrop-filter: blur(4px);
border-radius: 24px; border-radius: 24px;
backdrop-filter: blur(4px);
} }

View File

@@ -125,7 +125,7 @@ export const BaseDialog = ({
return ( return (
<AnimatePresence initial={false}> <AnimatePresence initial={false}>
{mounted && ( {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 && ( {!full && (
<motion.div <motion.div
className={cn( className={cn(
@@ -150,7 +150,7 @@ export const BaseDialog = ({
<motion.div <motion.div
className={cn( 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', full ? 'h-dvh w-full' : 'min-w-96 rounded-3xl shadow',
palette.mode === 'dark' palette.mode === 'dark'
? 'text-white shadow-zinc-900' ? 'text-white shadow-zinc-900'
@@ -202,7 +202,7 @@ export const BaseDialog = ({
<div <div
className={cn( 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', full && 'h-full px-6',
)} )}
style={{ style={{

View File

@@ -9,7 +9,7 @@ export const Header: FC<{ title?: ReactNode; header?: ReactNode }> = memo(
header?: ReactNode header?: ReactNode
}) { }) {
return ( 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> <h1 className="mb-1 text-4xl font-medium" data-tauri-drag-region>
{title} {title}
</h1> </h1>

View File

@@ -50,7 +50,7 @@ export const BasePage: FC<BasePageProps> = ({
</ScrollArea.Viewport> </ScrollArea.Viewport>
<ScrollArea.Scrollbar <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" orientation="vertical"
> >
<ScrollArea.Thumb className="ScrollArea-Thumb relative flex !w-1.5 flex-1 rounded-full" /> <ScrollArea.Thumb className="ScrollArea-Thumb relative flex !w-1.5 flex-1 rounded-full" />

View File

@@ -17,7 +17,7 @@ export const FloatingButton = ({
return ( return (
<Button <Button
className={cn( 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, className,
)} )}
sx={{ sx={{

View File

@@ -89,7 +89,7 @@ export const SidePage: FC<Props> = ({
<ScrollArea.Scrollbar <ScrollArea.Scrollbar
className={cn( 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', sideBar && '!top-14',
)} )}
orientation="vertical" orientation="vertical"
@@ -120,7 +120,7 @@ export const SidePage: FC<Props> = ({
</ScrollArea.Viewport> </ScrollArea.Viewport>
<ScrollArea.Scrollbar <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" orientation="vertical"
> >
<ScrollArea.Thumb className="!bg-scroller relative flex !w-1.5 flex-1 rounded-full" /> <ScrollArea.Thumb className="!bg-scroller relative flex !w-1.5 flex-1 rounded-full" />

View File

@@ -59,21 +59,21 @@
"devDependencies": { "devDependencies": {
"@commitlint/cli": "19.6.1", "@commitlint/cli": "19.6.1",
"@commitlint/config-conventional": "19.6.0", "@commitlint/config-conventional": "19.6.0",
"@eslint/compat": "1.2.5", "@eslint/compat": "1.2.6",
"@eslint/eslintrc": "3.2.0", "@eslint/eslintrc": "3.2.0",
"@ianvs/prettier-plugin-sort-imports": "4.4.1", "@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/fs-extra": "11.0.4",
"@types/lodash-es": "4.17.12", "@types/lodash-es": "4.17.12",
"@types/node": "22.10.10", "@types/node": "22.13.0",
"@typescript-eslint/eslint-plugin": "8.21.0", "@typescript-eslint/eslint-plugin": "8.22.0",
"@typescript-eslint/parser": "8.21.0", "@typescript-eslint/parser": "8.22.0",
"autoprefixer": "10.4.20", "autoprefixer": "10.4.20",
"conventional-changelog-conventionalcommits": "8.0.0", "conventional-changelog-conventionalcommits": "8.0.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"dedent": "1.5.3", "dedent": "1.5.3",
"eslint": "9.19.0", "eslint": "9.19.0",
"eslint-config-prettier": "9.1.0", "eslint-config-prettier": "10.0.1",
"eslint-config-standard": "17.1.0", "eslint-config-standard": "17.1.0",
"eslint-import-resolver-alias": "1.1.2", "eslint-import-resolver-alias": "1.1.2",
"eslint-plugin-html": "8.1.2", "eslint-plugin-html": "8.1.2",
@@ -86,7 +86,7 @@
"eslint-plugin-react-hooks": "5.1.0", "eslint-plugin-react-hooks": "5.1.0",
"globals": "15.14.0", "globals": "15.14.0",
"knip": "5.43.6", "knip": "5.43.6",
"lint-staged": "15.4.2", "lint-staged": "15.4.3",
"neostandard": "0.12.0", "neostandard": "0.12.0",
"npm-run-all2": "7.0.2", "npm-run-all2": "7.0.2",
"postcss": "8.5.1", "postcss": "8.5.1",
@@ -96,18 +96,18 @@
"prettier": "3.4.2", "prettier": "3.4.2",
"prettier-plugin-tailwindcss": "0.6.11", "prettier-plugin-tailwindcss": "0.6.11",
"prettier-plugin-toml": "2.0.1", "prettier-plugin-toml": "2.0.1",
"react-devtools": "6.0.1", "react-devtools": "6.1.0",
"stylelint": "16.13.2", "stylelint": "16.14.1",
"stylelint-config-html": "1.1.0", "stylelint-config-html": "1.1.0",
"stylelint-config-recess-order": "5.1.1", "stylelint-config-recess-order": "6.0.0",
"stylelint-config-standard": "36.0.1", "stylelint-config-standard": "37.0.0",
"stylelint-declaration-block-no-ignored-properties": "2.8.0", "stylelint-declaration-block-no-ignored-properties": "2.8.0",
"stylelint-order": "6.0.4", "stylelint-order": "6.0.4",
"stylelint-scss": "6.10.1", "stylelint-scss": "6.11.0",
"tailwindcss": "3.4.17", "tailwindcss": "4.0.3",
"tsx": "4.19.2", "tsx": "4.19.2",
"typescript": "5.7.3", "typescript": "5.7.3",
"typescript-eslint": "8.21.0" "typescript-eslint": "8.22.0"
}, },
"packageManager": "pnpm@10.1.0", "packageManager": "pnpm@10.1.0",
"engines": { "engines": {

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@
"p-retry": "6.2.1" "p-retry": "6.2.1"
}, },
"devDependencies": { "devDependencies": {
"@octokit/types": "13.7.0", "@octokit/types": "13.8.0",
"@types/adm-zip": "0.5.7", "@types/adm-zip": "0.5.7",
"adm-zip": "0.5.16", "adm-zip": "0.5.16",
"colorize-template": "1.0.0", "colorize-template": "1.0.0",

View File

@@ -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);

View File

@@ -352,15 +352,15 @@ o:value("119.28.28.28")
o:depends("direct_dns_mode", "tcp") o:depends("direct_dns_mode", "tcp")
o = s:taboption("DNS", Value, "direct_dns_dot", translate("Direct DNS DoT")) o = s:taboption("DNS", Value, "direct_dns_dot", translate("Direct DNS DoT"))
o.default = "tls://dot.pub@1.12.12.12" o.default = "tls://1.12.12.12"
o:value("tls://dot.pub@1.12.12.12") o:value("tls://1.12.12.12")
o:value("tls://dot.pub@120.53.53.53") o:value("tls://120.53.53.53")
o:value("tls://dot.360.cn@36.99.170.86") o:value("tls://36.99.170.86")
o:value("tls://dot.360.cn@101.198.191.4") o:value("tls://101.198.191.4")
o:value("tls://dns.alidns.com@223.5.5.5") o:value("tls://223.5.5.5")
o:value("tls://dns.alidns.com@223.6.6.6") o:value("tls://223.6.6.6")
o:value("tls://dns.alidns.com@2400:3200::1") o:value("tls://2400:3200::1")
o:value("tls://dns.alidns.com@2400:3200:baba::1") o:value("tls://2400:3200:baba::1")
o.validate = chinadns_dot_validate o.validate = chinadns_dot_validate
o:depends("direct_dns_mode", "dot") o:depends("direct_dns_mode", "dot")
@@ -502,17 +502,17 @@ o:depends({singbox_dns_mode = "tcp"})
---- DoT ---- DoT
o = s:taboption("DNS", Value, "remote_dns_dot", translate("Remote DNS DoT")) o = s:taboption("DNS", Value, "remote_dns_dot", translate("Remote DNS DoT"))
o.default = "tls://dns.google@8.8.4.4" o.default = "tls://1.1.1.1"
o:value("tls://1dot1dot1dot1.cloudflare-dns.com@1.0.0.1", "1.0.0.1 (CloudFlare)") o:value("tls://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://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://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://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://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://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://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://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://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:value("tls://208.67.220.220", "208.67.220.220 (OpenDNS)")
o.validate = chinadns_dot_validate o.validate = chinadns_dot_validate
o:depends("dns_mode", "dot") o:depends("dns_mode", "dot")

View File

@@ -718,7 +718,7 @@ function gen_config(var)
local blc_node_tag = "blc-" .. blc_node_id local blc_node_tag = "blc-" .. blc_node_id
local is_new_blc_node = true local is_new_blc_node = true
for _, outbound in ipairs(outbounds) do 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 is_new_blc_node = false
valid_nodes[#valid_nodes + 1] = outbound.tag valid_nodes[#valid_nodes + 1] = outbound.tag
break break
@@ -743,7 +743,7 @@ function gen_config(var)
if fallback_node_id then if fallback_node_id then
local is_new_node = true local is_new_node = true
for _, outbound in ipairs(outbounds) do 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 is_new_node = false
fallback_node_tag = outbound.tag fallback_node_tag = outbound.tag
break break

View File

@@ -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')" _args="${_args} direct_dns_tcp_server=$(config_t_get global direct_dns_tcp 223.5.5.5 | sed 's/:/#/g')"
;; ;;
dot) 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_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') 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}" _args="${_args} direct_dns_dot_server=$tmp_dot_ip#${tmp_dot_port:-853}"
@@ -1397,7 +1397,7 @@ start_dns() {
;; ;;
dot) dot)
if [ "$chinadns_tls" != "nil" ]; then 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} china_ng_local_dns=${DIRECT_DNS}
#当全局包括访问控制节点开启chinadns-ng时不启动新进程。 #当全局包括访问控制节点开启chinadns-ng时不启动新进程。
@@ -1519,7 +1519,7 @@ start_dns() {
TCP_PROXY_DNS=1 TCP_PROXY_DNS=1
if [ "$chinadns_tls" != "nil" ]; then if [ "$chinadns_tls" != "nil" ]; then
local china_ng_listen_port=${NEXT_DNS_LISTEN_PORT} 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_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') 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}" REMOTE_DNS="$tmp_dot_ip#${tmp_dot_port:-853}"
@@ -1864,7 +1864,7 @@ acl_app() {
;; ;;
dot) dot)
if [ "$(chinadns-ng -V | grep -i wolfssl)" != "nil" ]; then 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 fi
;; ;;
esac esac

View File

@@ -9,20 +9,21 @@ probe_file="/tmp/etc/passwall/haproxy/Probe_URL"
probeUrl="https://www.google.com/generate_204" probeUrl="https://www.google.com/generate_204"
if [ -f "$probe_file" ]; then if [ -f "$probe_file" ]; then
firstLine=$(head -n 1 "$probe_file" | tr -d ' \t') firstLine=$(head -n 1 "$probe_file" | tr -d ' \t')
if [ -n "$firstLine" ]; then [ -n "$firstLine" ] && probeUrl="$firstLine"
probeUrl="$firstLine"
fi
fi 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 case "$status" in
204|\ 200|204)
200) exit 0
status=200 ;;
*)
exit 1
;; ;;
esac esac
return_code=1
if [ "$status" = "200" ]; then
return_code=0
fi
exit ${return_code}

View File

@@ -28,9 +28,10 @@ test_url() {
local timeout=2 local timeout=2
[ -n "$3" ] && timeout=$3 [ -n "$3" ] && timeout=$3
local extra_params=$4 local extra_params=$4
curl --help all | grep "\-\-retry-all-errors" > /dev/null if /usr/bin/curl --help all | grep -q "\-\-retry-all-errors"; then
[ $? == 0 ] && extra_params="--retry-all-errors ${extra_params}" 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") fi
status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout ${timeout} --retry ${try} -w %{http_code} "$url")
case "$status" in case "$status" in
204) 204)
status=200 status=200

View File

@@ -50,17 +50,17 @@ type CacheFile interface {
StoreSelected(group string, selected string) error StoreSelected(group string, selected string) error
LoadGroupExpand(group string) (isExpand bool, loaded bool) LoadGroupExpand(group string) (isExpand bool, loaded bool)
StoreGroupExpand(group string, expand bool) error StoreGroupExpand(group string, expand bool) error
LoadRuleSet(tag string) *SavedRuleSet LoadRuleSet(tag string) *SavedBinary
SaveRuleSet(tag string, set *SavedRuleSet) error SaveRuleSet(tag string, set *SavedBinary) error
} }
type SavedRuleSet struct { type SavedBinary struct {
Content []byte Content []byte
LastUpdated time.Time LastUpdated time.Time
LastEtag string LastEtag string
} }
func (s *SavedRuleSet) MarshalBinary() ([]byte, error) { func (s *SavedBinary) MarshalBinary() ([]byte, error) {
var buffer bytes.Buffer var buffer bytes.Buffer
err := binary.Write(&buffer, binary.BigEndian, uint8(1)) err := binary.Write(&buffer, binary.BigEndian, uint8(1))
if err != nil { if err != nil {
@@ -81,7 +81,7 @@ func (s *SavedRuleSet) MarshalBinary() ([]byte, error) {
return buffer.Bytes(), nil return buffer.Bytes(), nil
} }
func (s *SavedRuleSet) UnmarshalBinary(data []byte) error { func (s *SavedBinary) UnmarshalBinary(data []byte) error {
reader := bytes.NewReader(data) reader := bytes.NewReader(data)
var version uint8 var version uint8
err := binary.Read(reader, binary.BigEndian, &version) err := binary.Read(reader, binary.BigEndian, &version)

View File

@@ -30,7 +30,7 @@ func init() {
} }
func generateTLSKeyPair(serverName string) error { 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 { if err != nil {
return err return err
} }

View File

@@ -11,8 +11,8 @@ import (
"time" "time"
) )
func GenerateCertificate(timeFunc func() time.Time, serverName string) (*tls.Certificate, error) { func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
privateKeyPem, publicKeyPem, err := GenerateKeyPair(timeFunc, serverName, timeFunc().Add(time.Hour)) privateKeyPem, publicKeyPem, err := GenerateCertificate(parent, parentKey, timeFunc, serverName, timeFunc().Add(time.Hour))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -23,7 +23,7 @@ func GenerateCertificate(timeFunc func() time.Time, serverName string) (*tls.Cer
return &certificate, err 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 { if timeFunc == nil {
timeFunc = time.Now timeFunc = time.Now
} }
@@ -47,7 +47,11 @@ func GenerateKeyPair(timeFunc func() time.Time, serverName string, expire time.T
}, },
DNSNames: []string{serverName}, 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 { if err != nil {
return return
} }

View File

@@ -222,7 +222,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
} }
if certificate == nil && key == nil && options.Insecure { if certificate == nil && key == nil && options.Insecure {
tlsConfig.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { 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 { } else {
if certificate == nil { if certificate == nil {

View File

@@ -284,8 +284,8 @@ func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error {
}) })
} }
func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedRuleSet { func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedBinary {
var savedSet adapter.SavedRuleSet var savedSet adapter.SavedBinary
err := c.DB.View(func(t *bbolt.Tx) error { err := c.DB.View(func(t *bbolt.Tx) error {
bucket := c.bucket(t, bucketRuleSet) bucket := c.bucket(t, bucketRuleSet)
if bucket == nil { if bucket == nil {
@@ -303,7 +303,7 @@ func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedRuleSet {
return &savedSet 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 { return c.DB.Batch(func(t *bbolt.Tx) error {
bucket, err := c.createBucket(t, bucketRuleSet) bucket, err := c.createBucket(t, bucketRuleSet)
if err != nil { if err != nil {

View File

@@ -7,11 +7,13 @@ var (
type Locale struct { type Locale struct {
// deprecated messages for graphical clients // deprecated messages for graphical clients
Locale string
DeprecatedMessage string DeprecatedMessage string
DeprecatedMessageNoLink string DeprecatedMessageNoLink string
} }
var defaultLocal = &Locale{ 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.", 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.", DeprecatedMessageNoLink: "%s is deprecated in sing-box %s and will be removed in sing-box %s.",
} }

View File

@@ -4,6 +4,7 @@ var warningMessageForEndUsers = "\n\n如果您不明白此消息意味着什么
func init() { func init() {
localeRegistry["zh_CN"] = &Locale{ localeRegistry["zh_CN"] = &Locale{
Locale: "zh_CN",
DeprecatedMessage: "%s 已在 sing-box %s 中被弃用,且将在 sing-box %s 中被移除,请参阅迁移指南。" + warningMessageForEndUsers, DeprecatedMessage: "%s 已在 sing-box %s 中被弃用,且将在 sing-box %s 中被移除,请参阅迁移指南。" + warningMessageForEndUsers,
DeprecatedMessageNoLink: "%s 已在 sing-box %s 中被弃用,且将在 sing-box %s 中被移除。" + warningMessageForEndUsers, DeprecatedMessageNoLink: "%s 已在 sing-box %s 中被弃用,且将在 sing-box %s 中被移除。" + warningMessageForEndUsers,
} }

View File

@@ -162,7 +162,7 @@ func (r *Router) Start(stage adapter.StartStage) error {
r.started = true r.started = true
return nil return nil
case adapter.StartStateStarted: case adapter.StartStateStarted:
for _, ruleSet := range r.ruleSetMap { for _, ruleSet := range r.ruleSets {
ruleSet.Cleanup() ruleSet.Cleanup()
} }
runtime.GC() runtime.GC()
@@ -180,6 +180,13 @@ func (r *Router) Close() error {
}) })
monitor.Finish() 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 return err
} }

View File

@@ -5,6 +5,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"github.com/sagernet/fswatch" "github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@@ -26,14 +27,16 @@ import (
var _ adapter.RuleSet = (*LocalRuleSet)(nil) var _ adapter.RuleSet = (*LocalRuleSet)(nil)
type LocalRuleSet struct { type LocalRuleSet struct {
ctx context.Context ctx context.Context
logger logger.Logger logger logger.Logger
tag string tag string
rules []adapter.HeadlessRule rules []adapter.HeadlessRule
metadata adapter.RuleSetMetadata metadata adapter.RuleSetMetadata
fileFormat string fileFormat string
watcher *fswatch.Watcher watcher *fswatch.Watcher
refs atomic.Int32 callbackAccess sync.Mutex
callbacks list.List[adapter.RuleSetUpdateCallback]
refs atomic.Int32
} }
func NewLocalRuleSet(ctx context.Context, logger logger.Logger, options option.RuleSet) (*LocalRuleSet, error) { 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 return nil, err
} }
} else { } 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 { if err != nil {
return nil, err return nil, err
} }
}
if options.Type == C.RuleSetTypeLocal {
filePath, _ := filepath.Abs(options.LocalOptions.Path)
watcher, err := fswatch.NewWatcher(fswatch.Options{ watcher, err := fswatch.NewWatcher(fswatch.Options{
Path: []string{filePath}, Path: []string{filePath},
Callback: func(path string) { Callback: func(path string) {
@@ -141,6 +143,12 @@ func (s *LocalRuleSet) reloadRules(headlessRules []option.HeadlessRule) error {
metadata.ContainsIPCIDRRule = hasHeadlessRule(headlessRules, isIPCIDRHeadlessRule) metadata.ContainsIPCIDRRule = hasHeadlessRule(headlessRules, isIPCIDRHeadlessRule)
s.rules = rules s.rules = rules
s.metadata = metadata s.metadata = metadata
s.callbackAccess.Lock()
callbacks := s.callbacks.Array()
s.callbackAccess.Unlock()
for _, callback := range callbacks {
callback(s)
}
return nil return nil
} }
@@ -173,10 +181,15 @@ func (s *LocalRuleSet) Cleanup() {
} }
func (s *LocalRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] { 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]) { 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 { func (s *LocalRuleSet) Close() error {

View File

@@ -35,23 +35,23 @@ import (
var _ adapter.RuleSet = (*RemoteRuleSet)(nil) var _ adapter.RuleSet = (*RemoteRuleSet)(nil)
type RemoteRuleSet struct { type RemoteRuleSet struct {
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
outboundManager adapter.OutboundManager logger logger.ContextLogger
logger logger.ContextLogger outbound adapter.OutboundManager
options option.RuleSet options option.RuleSet
metadata adapter.RuleSetMetadata metadata adapter.RuleSetMetadata
updateInterval time.Duration updateInterval time.Duration
dialer N.Dialer dialer N.Dialer
rules []adapter.HeadlessRule rules []adapter.HeadlessRule
lastUpdated time.Time lastUpdated time.Time
lastEtag string lastEtag string
updateTicker *time.Ticker updateTicker *time.Ticker
cacheFile adapter.CacheFile cacheFile adapter.CacheFile
pauseManager pause.Manager pauseManager pause.Manager
callbackAccess sync.Mutex callbackAccess sync.Mutex
callbacks list.List[adapter.RuleSetUpdateCallback] callbacks list.List[adapter.RuleSetUpdateCallback]
refs atomic.Int32 refs atomic.Int32
} }
func NewRemoteRuleSet(ctx context.Context, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet { 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 updateInterval = 24 * time.Hour
} }
return &RemoteRuleSet{ return &RemoteRuleSet{
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
outboundManager: service.FromContext[adapter.OutboundManager](ctx), outbound: service.FromContext[adapter.OutboundManager](ctx),
logger: logger, logger: logger,
options: options, options: options,
updateInterval: updateInterval, updateInterval: updateInterval,
pauseManager: service.FromContext[pause.Manager](ctx), 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) s.cacheFile = service.FromContext[adapter.CacheFile](s.ctx)
var dialer N.Dialer var dialer N.Dialer
if s.options.RemoteOptions.DownloadDetour != "" { 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 { 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 dialer = outbound
} else { } else {
dialer = s.outboundManager.Default() dialer = s.outbound.Default()
} }
s.dialer = dialer s.dialer = dialer
if s.cacheFile != nil { if s.cacheFile != nil {
@@ -292,7 +292,7 @@ func (s *RemoteRuleSet) fetchOnce(ctx context.Context, startContext *adapter.HTT
} }
s.lastUpdated = time.Now() s.lastUpdated = time.Now()
if s.cacheFile != nil { 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, LastUpdated: s.lastUpdated,
Content: content, Content: content,
LastEtag: s.lastEtag, LastEtag: s.lastEtag,

View File

@@ -1,6 +1,6 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_VERSION:=1.17.4 PKG_VERSION:=1.17.5
LUCI_TITLE:=LuCI Support for mihomo LUCI_TITLE:=LuCI Support for mihomo
LUCI_DEPENDS:=+luci-base +mihomo LUCI_DEPENDS:=+luci-base +mihomo

View File

@@ -155,6 +155,16 @@ return view.extend({
o.retain = true; o.retain = true;
o.depends('tun_gso', '1'); 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 = s.taboption('tun', form.Flag, 'tun_endpoint_independent_nat', '*' + ' ' + _('Endpoint Independent NAT'));
o.rmempty = false; o.rmempty = false;

View File

@@ -26,7 +26,7 @@ msgstr ""
msgid "Allow Lan" msgid "Allow Lan"
msgstr "" 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 #: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/proxy.js:65
msgid "Allow Mode" msgid "Allow Mode"
msgstr "" msgstr ""
@@ -48,7 +48,7 @@ msgstr ""
msgid "Auto" msgid "Auto"
msgstr "" 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 #: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/proxy.js:66
msgid "Block Mode" msgid "Block Mode"
msgstr "" msgstr ""
@@ -111,15 +111,15 @@ msgstr ""
msgid "Cron Expression" msgid "Cron Expression"
msgstr "" 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" msgid "DNS Config"
msgstr "" 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" msgid "DNS Mode"
msgstr "" 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" msgid "DNS Port"
msgstr "" msgstr ""
@@ -159,11 +159,11 @@ msgstr ""
msgid "Disable Safe Path Check" msgid "Disable Safe Path Check"
msgstr "" 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" msgid "DoH Prefer HTTP/3"
msgstr "" 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" msgid "Domain Name"
msgstr "" msgstr ""
@@ -171,19 +171,23 @@ msgstr ""
msgid "Edit Authentications" msgid "Edit Authentications"
msgstr "" 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" msgid "Edit Fake-IP Filters"
msgstr "" 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" msgid "Edit Hosts"
msgstr "" 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" msgid "Edit Nameserver Policies"
msgstr "" 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" msgid "Edit Nameservers"
msgstr "" 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: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: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: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:232
#: 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:251
#: 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:274
#: 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:284
#: 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:316
#: 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
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/proxy.js:31 #: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/proxy.js:31
msgid "Enable" msgid "Enable"
msgstr "" 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" msgid "Endpoint Independent NAT"
msgstr "" msgstr ""
@@ -222,15 +226,15 @@ msgstr ""
msgid "External Control Config" msgid "External Control Config"
msgstr "" 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" msgid "Fake-IP Cache"
msgstr "" 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" msgid "Fake-IP Filter Mode"
msgstr "" 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" msgid "Fake-IP Range"
msgstr "" msgstr ""
@@ -255,7 +259,7 @@ msgstr ""
msgid "File:" msgid "File:"
msgstr "" 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" msgid "Force Sniff Domain Name"
msgstr "" msgstr ""
@@ -271,39 +275,39 @@ msgstr ""
msgid "General Config" msgid "General Config"
msgstr "" 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" msgid "GeoData Loader"
msgstr "" 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" msgid "GeoIP Format"
msgstr "" 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" msgid "GeoIP(ASN) Url"
msgstr "" 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" msgid "GeoIP(DAT) Url"
msgstr "" 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" msgid "GeoIP(MMDB) Url"
msgstr "" 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" msgid "GeoSite Url"
msgstr "" 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" msgid "GeoX Auto Update"
msgstr "" 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" msgid "GeoX Config"
msgstr "" 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" msgid "GeoX Update Interval"
msgstr "" msgstr ""
@@ -323,7 +327,7 @@ msgstr ""
msgid "How To Use" msgid "How To Use"
msgstr "" 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" msgid "IP"
msgstr "" msgstr ""
@@ -336,7 +340,7 @@ msgid "IPv4 Proxy"
msgstr "" 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: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" msgid "IPv6"
msgstr "" msgstr ""
@@ -348,7 +352,7 @@ msgstr ""
msgid "IPv6 Proxy" msgid "IPv6 Proxy"
msgstr "" 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" msgid "Ignore Sniff Domain Name"
msgstr "" msgstr ""
@@ -385,11 +389,11 @@ msgstr ""
msgid "Match Process" msgid "Match Process"
msgstr "" 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" msgid "Matcher"
msgstr "" 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" msgid "Memory Conservative Loader"
msgstr "" msgstr ""
@@ -407,7 +411,7 @@ msgstr ""
msgid "Mixin Config" msgid "Mixin Config"
msgstr "" 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" msgid "Mixin File Content"
msgstr "" msgstr ""
@@ -420,8 +424,8 @@ msgstr ""
msgid "Mode" msgid "Mode"
msgstr "" 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:261
#: 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:280
msgid "Nameserver" msgid "Nameserver"
msgstr "" msgstr ""
@@ -441,36 +445,40 @@ msgstr ""
msgid "Overwrite Authentication" msgid "Overwrite Authentication"
msgstr "" 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:158
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:320 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" msgid "Overwrite Destination"
msgstr "" 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" msgid "Overwrite Fake-IP Filter"
msgstr "" 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" msgid "Overwrite Force Sniff Domain Name"
msgstr "" 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" msgid "Overwrite Hosts"
msgstr "" 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" msgid "Overwrite Ignore Sniff Domain Name"
msgstr "" 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" msgid "Overwrite Nameserver"
msgstr "" 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" msgid "Overwrite Nameserver Policy"
msgstr "" 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" msgid "Overwrite Sniff By Protocol"
msgstr "" msgstr ""
@@ -478,11 +486,11 @@ msgstr ""
msgid "Password" msgid "Password"
msgstr "" 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" msgid "Please go to the editor tab to edit the file for mixin"
msgstr "" 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" msgid "Port"
msgstr "" msgstr ""
@@ -499,7 +507,7 @@ msgstr ""
msgid "Profile for Startup" msgid "Profile for Startup"
msgstr "" 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" msgid "Protocol"
msgstr "" msgstr ""
@@ -524,7 +532,7 @@ msgstr ""
msgid "Remote" msgid "Remote"
msgstr "" 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" msgid "Respect Rules"
msgstr "" msgstr ""
@@ -561,24 +569,24 @@ msgstr ""
msgid "Scroll To Bottom" msgid "Scroll To Bottom"
msgstr "" msgstr ""
#: applications/luci-app-mihomo/htdocs/luci-static/resources/tools/mihomo.js:99 #: applications/luci-app-mihomo/htdocs/luci-static/resources/tools/mihomo.js:100
#: applications/luci-app-mihomo/htdocs/luci-static/resources/tools/mihomo.js:117 #: applications/luci-app-mihomo/htdocs/luci-static/resources/tools/mihomo.js:118
msgid "Service is not running." msgid "Service is not running."
msgstr "" 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" msgid "Sniff By Protocol"
msgstr "" 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" msgid "Sniff Pure IP"
msgstr "" 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" msgid "Sniff Redir-Host"
msgstr "" 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" msgid "Sniffer Config"
msgstr "" msgstr ""
@@ -586,7 +594,7 @@ msgstr ""
msgid "Stack" msgid "Stack"
msgstr "" 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" msgid "Standard Loader"
msgstr "" msgstr ""
@@ -665,7 +673,7 @@ msgstr ""
msgid "Transparent Proxy with Mihomo on OpenWrt." msgid "Transparent Proxy with Mihomo on OpenWrt."
msgstr "" 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" msgid "Type"
msgstr "" msgstr ""
@@ -706,11 +714,11 @@ msgstr ""
msgid "Upload Profile" msgid "Upload Profile"
msgstr "" 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" msgid "Use Hosts"
msgstr "" 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" msgid "Use System Hosts"
msgstr "" msgstr ""

View File

@@ -33,7 +33,7 @@ msgstr "全部端口"
msgid "Allow Lan" msgid "Allow Lan"
msgstr "允许局域网访问" 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 #: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/proxy.js:65
msgid "Allow Mode" msgid "Allow Mode"
msgstr "白名单模式" msgstr "白名单模式"
@@ -55,7 +55,7 @@ msgstr "插件版本"
msgid "Auto" msgid "Auto"
msgstr "自动" 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 #: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/proxy.js:66
msgid "Block Mode" msgid "Block Mode"
msgstr "黑名单模式" msgstr "黑名单模式"
@@ -118,15 +118,15 @@ msgstr "核心版本"
msgid "Cron Expression" msgid "Cron Expression"
msgstr "Cron 表达式" 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" msgid "DNS Config"
msgstr "DNS 配置" 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" msgid "DNS Mode"
msgstr "DNS 模式" 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" msgid "DNS Port"
msgstr "DNS 端口" msgstr "DNS 端口"
@@ -166,11 +166,11 @@ msgstr "禁用回环检测"
msgid "Disable Safe Path Check" msgid "Disable Safe Path Check"
msgstr "禁用安全路径检查" 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" msgid "DoH Prefer HTTP/3"
msgstr "DoH 优先 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" msgid "Domain Name"
msgstr "域名" msgstr "域名"
@@ -178,19 +178,23 @@ msgstr "域名"
msgid "Edit Authentications" msgid "Edit Authentications"
msgstr "编辑身份验证" 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" msgid "Edit Fake-IP Filters"
msgstr "编辑 Fake-IP 过滤列表" 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" msgid "Edit Hosts"
msgstr "编辑 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" msgid "Edit Nameserver Policies"
msgstr "编辑 DNS 服务器查询策略" 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" msgid "Edit Nameservers"
msgstr "编辑 DNS 服务器" 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: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: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: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:232
#: 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:251
#: 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:274
#: 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:284
#: 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:316
#: 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
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/proxy.js:31 #: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/proxy.js:31
msgid "Enable" msgid "Enable"
msgstr "启用" 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" msgid "Endpoint Independent NAT"
msgstr "独立于端点的 NAT" msgstr "独立于端点的 NAT"
@@ -229,15 +233,15 @@ msgstr "到期时间"
msgid "External Control Config" msgid "External Control Config"
msgstr "外部控制配置" 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" msgid "Fake-IP Cache"
msgstr "Fake-IP 缓存" 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" msgid "Fake-IP Filter Mode"
msgstr "Fake-IP 过滤模式" 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" msgid "Fake-IP Range"
msgstr "Fake-IP 范围" msgstr "Fake-IP 范围"
@@ -262,7 +266,7 @@ msgstr "IPv6 保留地址"
msgid "File:" msgid "File:"
msgstr "文件:" 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" msgid "Force Sniff Domain Name"
msgstr "强制嗅探的域名" msgstr "强制嗅探的域名"
@@ -278,39 +282,39 @@ msgstr "分段最大长度"
msgid "General Config" msgid "General Config"
msgstr "全局配置" 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" msgid "GeoData Loader"
msgstr "GeoData 加载器" 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" msgid "GeoIP Format"
msgstr "GeoIP 格式" 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" msgid "GeoIP(ASN) Url"
msgstr "GeoIP(ASN) 下载地址" 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" msgid "GeoIP(DAT) Url"
msgstr "GeoIP(DAT) 下载地址" 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" msgid "GeoIP(MMDB) Url"
msgstr "GeoIP(MMDB) 下载地址" 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" msgid "GeoSite Url"
msgstr "GeoSite 下载地址" 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" msgid "GeoX Auto Update"
msgstr "定时更新GeoX文件" 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" msgid "GeoX Config"
msgstr "GeoX 配置" 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" msgid "GeoX Update Interval"
msgstr "GeoX 文件更新间隔" msgstr "GeoX 文件更新间隔"
@@ -330,7 +334,7 @@ msgstr "HTTP 端口"
msgid "How To Use" msgid "How To Use"
msgstr "使用说明" 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" msgid "IP"
msgstr "" msgstr ""
@@ -343,7 +347,7 @@ msgid "IPv4 Proxy"
msgstr "IPv4 代理" 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: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" msgid "IPv6"
msgstr "" msgstr ""
@@ -355,7 +359,7 @@ msgstr "IPv6 DNS 劫持"
msgid "IPv6 Proxy" msgid "IPv6 Proxy"
msgstr "IPv6 代理" 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" msgid "Ignore Sniff Domain Name"
msgstr "忽略嗅探的域名" msgstr "忽略嗅探的域名"
@@ -392,11 +396,11 @@ msgstr "最大传输单元"
msgid "Match Process" msgid "Match Process"
msgstr "匹配进程" 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" msgid "Matcher"
msgstr "匹配" 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" msgid "Memory Conservative Loader"
msgstr "为内存受限设备优化的加载器" msgstr "为内存受限设备优化的加载器"
@@ -414,7 +418,7 @@ msgstr "混合端口"
msgid "Mixin Config" msgid "Mixin Config"
msgstr "混入配置" 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" msgid "Mixin File Content"
msgstr "混入文件内容" msgstr "混入文件内容"
@@ -427,8 +431,8 @@ msgstr "混入选项"
msgid "Mode" msgid "Mode"
msgstr "模式" 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:261
#: 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:280
msgid "Nameserver" msgid "Nameserver"
msgstr "DNS 服务器" msgstr "DNS 服务器"
@@ -448,36 +452,40 @@ msgstr "出站接口"
msgid "Overwrite Authentication" msgid "Overwrite Authentication"
msgstr "覆盖身份验证" 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:158
#: applications/luci-app-mihomo/htdocs/luci-static/resources/view/mihomo/mixin.js:320 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" msgid "Overwrite Destination"
msgstr "将嗅探结果作为连接目标" 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" msgid "Overwrite Fake-IP Filter"
msgstr "覆盖 Fake-IP 过滤列表" 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" msgid "Overwrite Force Sniff Domain Name"
msgstr "覆盖强制嗅探的域名" 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" msgid "Overwrite Hosts"
msgstr "覆盖 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" msgid "Overwrite Ignore Sniff Domain Name"
msgstr "覆盖忽略嗅探的域名" 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" msgid "Overwrite Nameserver"
msgstr "覆盖 DNS 服务器" 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" msgid "Overwrite Nameserver Policy"
msgstr "覆盖 DNS 服务器查询策略" 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" msgid "Overwrite Sniff By Protocol"
msgstr "覆盖按协议嗅探" msgstr "覆盖按协议嗅探"
@@ -485,11 +493,11 @@ msgstr "覆盖按协议嗅探"
msgid "Password" msgid "Password"
msgstr "密码" 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" msgid "Please go to the editor tab to edit the file for mixin"
msgstr "请前往编辑器标签编辑用于混入的文件" 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" msgid "Port"
msgstr "端口" msgstr "端口"
@@ -506,7 +514,7 @@ msgstr "配置文件"
msgid "Profile for Startup" msgid "Profile for Startup"
msgstr "用于启动的配置文件" 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" msgid "Protocol"
msgstr "协议" msgstr "协议"
@@ -531,7 +539,7 @@ msgstr "重载服务"
msgid "Remote" msgid "Remote"
msgstr "远程" 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" msgid "Respect Rules"
msgstr "遵循分流规则" msgstr "遵循分流规则"
@@ -568,24 +576,24 @@ msgstr "定时重启"
msgid "Scroll To Bottom" msgid "Scroll To Bottom"
msgstr "滚动到底部" msgstr "滚动到底部"
#: applications/luci-app-mihomo/htdocs/luci-static/resources/tools/mihomo.js:99 #: applications/luci-app-mihomo/htdocs/luci-static/resources/tools/mihomo.js:100
#: applications/luci-app-mihomo/htdocs/luci-static/resources/tools/mihomo.js:117 #: applications/luci-app-mihomo/htdocs/luci-static/resources/tools/mihomo.js:118
msgid "Service is not running." msgid "Service is not running."
msgstr "服务未在运行。" 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" msgid "Sniff By Protocol"
msgstr "按协议嗅探" 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" msgid "Sniff Pure IP"
msgstr "嗅探纯 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" msgid "Sniff Redir-Host"
msgstr "嗅探 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" msgid "Sniffer Config"
msgstr "嗅探器配置" msgstr "嗅探器配置"
@@ -593,7 +601,7 @@ msgstr "嗅探器配置"
msgid "Stack" msgid "Stack"
msgstr "栈" 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" msgid "Standard Loader"
msgstr "标准加载器" msgstr "标准加载器"
@@ -672,7 +680,7 @@ msgstr "透明代理"
msgid "Transparent Proxy with Mihomo on OpenWrt." msgid "Transparent Proxy with Mihomo on OpenWrt."
msgstr "在 OpenWrt 上使用 Mihomo 进行透明代理。" 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" msgid "Type"
msgstr "类型" msgstr "类型"
@@ -713,11 +721,11 @@ msgstr "更新面板"
msgid "Upload Profile" msgid "Upload Profile"
msgstr "上传配置文件" 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" msgid "Use Hosts"
msgstr "使用 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" msgid "Use System Hosts"
msgstr "使用系统的 Hosts" msgstr "使用系统的 Hosts"

Some files were not shown because too many files have changed in this diff Show More