mirror of
https://mirror.suhoan.cn/https://github.com/EasyTier/EasyTier.git
synced 2025-12-15 06:07:23 +08:00
refactor(gui): refactor gui to use RemoteClient trait and RemoteManagement component (#1489)
* refactor(gui): refactor gui to use RemoteClient trait and RemoteManagement component * feat(gui): Add network config saving and refactor RemoteManagement
This commit is contained in:
@@ -3,28 +3,44 @@
|
||||
|
||||
mod elevate;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use easytier::proto::api::manage::{
|
||||
CollectNetworkInfoResponse, ValidateConfigResponse, WebClientService,
|
||||
WebClientServiceClientFactory,
|
||||
};
|
||||
use easytier::rpc_service::remote_client::{
|
||||
ListNetworkInstanceIdsJsonResp, ListNetworkProps, RemoteClientManager, Storage,
|
||||
};
|
||||
use easytier::{
|
||||
common::config::{ConfigLoader, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader},
|
||||
instance_manager::NetworkInstanceManager,
|
||||
launcher::{ConfigSource, NetworkConfig, NetworkInstanceRunningInfo},
|
||||
launcher::NetworkConfig,
|
||||
rpc_service::ApiRpcServer,
|
||||
tunnel::ring::RingTunnelListener,
|
||||
utils::{self, NewFilterSender},
|
||||
};
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use tauri::Manager as _;
|
||||
|
||||
pub const AUTOSTART_ARG: &str = "--autostart";
|
||||
use tauri::{AppHandle, Emitter, Manager as _};
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
||||
|
||||
static INSTANCE_MANAGER: once_cell::sync::Lazy<NetworkInstanceManager> =
|
||||
once_cell::sync::Lazy::new(NetworkInstanceManager::new);
|
||||
pub const AUTOSTART_ARG: &str = "--autostart";
|
||||
|
||||
static INSTANCE_MANAGER: once_cell::sync::Lazy<Arc<NetworkInstanceManager>> =
|
||||
once_cell::sync::Lazy::new(|| Arc::new(NetworkInstanceManager::new()));
|
||||
|
||||
static mut LOGGER_LEVEL_SENDER: once_cell::sync::Lazy<Option<NewFilterSender>> =
|
||||
once_cell::sync::Lazy::new(Default::default);
|
||||
|
||||
static RPC_RING_UUID: once_cell::sync::Lazy<uuid::Uuid> =
|
||||
once_cell::sync::Lazy::new(uuid::Uuid::new_v4);
|
||||
|
||||
static CLIENT_MANAGER: once_cell::sync::OnceCell<manager::GUIClientManager> =
|
||||
once_cell::sync::OnceCell::new();
|
||||
|
||||
#[tauri::command]
|
||||
fn easytier_version() -> Result<String, String> {
|
||||
Ok(easytier::VERSION.to_string())
|
||||
@@ -47,14 +63,6 @@ fn set_dock_visibility(app: tauri::AppHandle, visible: bool) -> Result<(), Strin
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn is_autostart() -> Result<bool, String> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
println!("{:?}", args);
|
||||
Ok(args.contains(&AUTOSTART_ARG.to_owned()))
|
||||
}
|
||||
|
||||
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
|
||||
#[tauri::command]
|
||||
fn parse_network_config(cfg: NetworkConfig) -> Result<String, String> {
|
||||
let toml = cfg.gen_config().map_err(|e| e.to_string())?;
|
||||
@@ -69,47 +77,48 @@ fn generate_network_config(toml_config: String) -> Result<NetworkConfig, String>
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn run_network_instance(cfg: NetworkConfig) -> Result<(), String> {
|
||||
async fn run_network_instance(app: AppHandle, cfg: NetworkConfig) -> Result<(), String> {
|
||||
let instance_id = cfg.instance_id().to_string();
|
||||
let cfg = cfg.gen_config().map_err(|e| e.to_string())?;
|
||||
INSTANCE_MANAGER
|
||||
.run_network_instance(cfg, ConfigSource::GUI)
|
||||
.map_err(|e| e.to_string())?;
|
||||
println!("instance {} started", instance_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn retain_network_instance(instance_ids: Vec<String>) -> Result<(), String> {
|
||||
let instance_ids = instance_ids
|
||||
.into_iter()
|
||||
.filter_map(|id| uuid::Uuid::parse_str(&id).ok())
|
||||
.collect();
|
||||
let retained = INSTANCE_MANAGER
|
||||
.retain_network_instance(instance_ids)
|
||||
app.emit("pre_run_network_instance", cfg.instance_id())
|
||||
.map_err(|e| e.to_string())?;
|
||||
println!("instance {:?} retained", retained);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn collect_network_infos() -> Result<BTreeMap<String, NetworkInstanceRunningInfo>, String> {
|
||||
let infos = INSTANCE_MANAGER
|
||||
.collect_network_infos()
|
||||
#[cfg(target_os = "android")]
|
||||
if cfg.no_tun() == false {
|
||||
CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
.disable_instances_with_tun(&app)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
.handle_run_network_instance(app.clone(), cfg)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut ret = BTreeMap::new();
|
||||
for (uuid, info) in infos {
|
||||
ret.insert(uuid.to_string(), info);
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
app.emit("post_run_network_instance", instance_id)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_os_hostname() -> Result<String, String> {
|
||||
Ok(gethostname::gethostname().to_string_lossy().to_string())
|
||||
async fn collect_network_info(
|
||||
app: AppHandle,
|
||||
instance_id: String,
|
||||
) -> Result<CollectNetworkInfoResponse, String> {
|
||||
let instance_id = instance_id
|
||||
.parse()
|
||||
.map_err(|e: uuid::Error| e.to_string())?;
|
||||
CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
.handle_collect_network_info(app, Some(vec![instance_id]))
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -121,10 +130,121 @@ fn set_logging_level(level: String) -> Result<(), String> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_tun_fd(instance_id: String, fd: i32) -> Result<(), String> {
|
||||
let uuid = uuid::Uuid::parse_str(&instance_id).map_err(|e| e.to_string())?;
|
||||
INSTANCE_MANAGER
|
||||
.set_tun_fd(&uuid, fd)
|
||||
fn set_tun_fd(fd: i32) -> Result<(), String> {
|
||||
if let Some(uuid) = CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
.get_enabled_instances_with_tun_ids()
|
||||
.next()
|
||||
{
|
||||
INSTANCE_MANAGER
|
||||
.set_tun_fd(&uuid, fd)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn list_network_instance_ids(
|
||||
app: AppHandle,
|
||||
) -> Result<ListNetworkInstanceIdsJsonResp, String> {
|
||||
CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
.handle_list_network_instance_ids(app)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn remove_network_instance(app: AppHandle, instance_id: String) -> Result<(), String> {
|
||||
let instance_id = instance_id
|
||||
.parse()
|
||||
.map_err(|e: uuid::Error| e.to_string())?;
|
||||
CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
.handle_remove_network_instances(app.clone(), vec![instance_id])
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
.notify_vpn_stop_if_no_tun(&app)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn update_network_config_state(
|
||||
app: AppHandle,
|
||||
instance_id: String,
|
||||
disabled: bool,
|
||||
) -> Result<(), String> {
|
||||
let instance_id = instance_id
|
||||
.parse()
|
||||
.map_err(|e: uuid::Error| e.to_string())?;
|
||||
CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
.handle_update_network_state(app.clone(), instance_id, disabled)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
if disabled {
|
||||
CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
.notify_vpn_stop_if_no_tun(&app)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn save_network_config(app: AppHandle, cfg: NetworkConfig) -> Result<(), String> {
|
||||
let instance_id = cfg
|
||||
.instance_id()
|
||||
.parse()
|
||||
.map_err(|e: uuid::Error| e.to_string())?;
|
||||
CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
.handle_save_network_config(app, instance_id, cfg)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn validate_config(
|
||||
app: AppHandle,
|
||||
config: NetworkConfig,
|
||||
) -> Result<ValidateConfigResponse, String> {
|
||||
CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
.handle_validate_config(app, config)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_config(app: AppHandle, instance_id: String) -> Result<NetworkConfig, String> {
|
||||
let cfg = CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
.storage
|
||||
.get_network_config(app, &instance_id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| format!("Config not found for instance ID: {}", instance_id))?;
|
||||
Ok(cfg.1)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn load_configs(configs: Vec<NetworkConfig>, enabled_networks: Vec<String>) -> Result<(), String> {
|
||||
CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
.storage
|
||||
.load_configs(configs, enabled_networks)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -166,6 +286,266 @@ fn check_sudo() -> bool {
|
||||
is_elevated
|
||||
}
|
||||
|
||||
mod manager {
|
||||
use super::*;
|
||||
use async_trait::async_trait;
|
||||
use dashmap::{DashMap, DashSet};
|
||||
use easytier::launcher::{ConfigSource, NetworkConfig};
|
||||
use easytier::proto::rpc_impl::bidirect::BidirectRpcManager;
|
||||
use easytier::proto::rpc_types::controller::BaseController;
|
||||
use easytier::rpc_service::remote_client::PersistentConfig;
|
||||
use easytier::tunnel::ring::RingTunnelConnector;
|
||||
use easytier::tunnel::TunnelConnector;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct GUIConfig(String, pub(crate) NetworkConfig);
|
||||
impl PersistentConfig<anyhow::Error> for GUIConfig {
|
||||
fn get_network_inst_id(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
fn get_network_config(&self) -> Result<NetworkConfig, anyhow::Error> {
|
||||
Ok(self.1.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct GUIStorage {
|
||||
network_configs: DashMap<Uuid, GUIConfig>,
|
||||
enabled_networks: DashSet<Uuid>,
|
||||
}
|
||||
impl GUIStorage {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
network_configs: DashMap::new(),
|
||||
enabled_networks: DashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn load_configs(
|
||||
&self,
|
||||
configs: Vec<NetworkConfig>,
|
||||
enabled_networks: Vec<String>,
|
||||
) -> anyhow::Result<()> {
|
||||
self.network_configs.clear();
|
||||
for cfg in configs {
|
||||
let instance_id = cfg.instance_id();
|
||||
self.network_configs.insert(
|
||||
instance_id.parse()?,
|
||||
GUIConfig(instance_id.to_string(), cfg),
|
||||
);
|
||||
}
|
||||
|
||||
self.enabled_networks.clear();
|
||||
INSTANCE_MANAGER
|
||||
.filter_network_instance(|_, _| true)
|
||||
.into_iter()
|
||||
.for_each(|id| {
|
||||
self.enabled_networks.insert(id);
|
||||
});
|
||||
for id in enabled_networks {
|
||||
if let Ok(uuid) = id.parse() {
|
||||
if !self.enabled_networks.contains(&uuid) {
|
||||
let config = self
|
||||
.network_configs
|
||||
.get(&uuid)
|
||||
.map(|i| i.value().1.gen_config())
|
||||
.ok_or_else(|| anyhow::anyhow!("Config not found"))??;
|
||||
INSTANCE_MANAGER.run_network_instance(config, ConfigSource::GUI)?;
|
||||
self.enabled_networks.insert(uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save_configs(&self, app: &AppHandle) -> anyhow::Result<()> {
|
||||
let configs: Result<Vec<String>, _> = self
|
||||
.network_configs
|
||||
.iter()
|
||||
.map(|entry| serde_json::to_string(&entry.value().1))
|
||||
.collect();
|
||||
let payload = format!("[{}]", configs?.join(","));
|
||||
app.emit_str("save_configs", payload)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save_enabled_networks(&self, app: &AppHandle) -> anyhow::Result<()> {
|
||||
let payload: Vec<String> = self
|
||||
.enabled_networks
|
||||
.iter()
|
||||
.map(|entry| entry.key().to_string())
|
||||
.collect();
|
||||
app.emit("save_enabled_networks", payload)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save_config(
|
||||
&self,
|
||||
app: &AppHandle,
|
||||
inst_id: Uuid,
|
||||
cfg: NetworkConfig,
|
||||
) -> anyhow::Result<()> {
|
||||
let config = GUIConfig(inst_id.to_string(), cfg);
|
||||
self.network_configs.insert(inst_id, config);
|
||||
self.save_configs(app)
|
||||
}
|
||||
}
|
||||
#[async_trait]
|
||||
impl Storage<AppHandle, GUIConfig, anyhow::Error> for GUIStorage {
|
||||
async fn insert_or_update_user_network_config(
|
||||
&self,
|
||||
app: AppHandle,
|
||||
network_inst_id: Uuid,
|
||||
network_config: NetworkConfig,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
self.save_config(&app, network_inst_id, network_config)?;
|
||||
self.enabled_networks.insert(network_inst_id);
|
||||
self.save_enabled_networks(&app)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_network_configs(
|
||||
&self,
|
||||
app: AppHandle,
|
||||
network_inst_ids: &[Uuid],
|
||||
) -> Result<(), anyhow::Error> {
|
||||
for network_inst_id in network_inst_ids {
|
||||
self.network_configs.remove(network_inst_id);
|
||||
self.enabled_networks.remove(network_inst_id);
|
||||
}
|
||||
self.save_configs(&app)
|
||||
}
|
||||
|
||||
async fn update_network_config_state(
|
||||
&self,
|
||||
app: AppHandle,
|
||||
network_inst_id: Uuid,
|
||||
disabled: bool,
|
||||
) -> Result<GUIConfig, anyhow::Error> {
|
||||
if disabled {
|
||||
self.enabled_networks.remove(&network_inst_id);
|
||||
} else {
|
||||
self.enabled_networks.insert(network_inst_id);
|
||||
}
|
||||
self.save_enabled_networks(&app)?;
|
||||
let cfg = self
|
||||
.network_configs
|
||||
.get(&network_inst_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("Config not found"))?;
|
||||
Ok(cfg.value().clone())
|
||||
}
|
||||
|
||||
async fn list_network_configs(
|
||||
&self,
|
||||
_: AppHandle,
|
||||
props: ListNetworkProps,
|
||||
) -> Result<Vec<GUIConfig>, anyhow::Error> {
|
||||
let mut ret = Vec::new();
|
||||
for entry in self.network_configs.iter() {
|
||||
let id: Uuid = entry.key().to_owned();
|
||||
match props {
|
||||
ListNetworkProps::All => {
|
||||
ret.push(entry.value().clone());
|
||||
}
|
||||
ListNetworkProps::EnabledOnly => {
|
||||
if self.enabled_networks.contains(&id) {
|
||||
ret.push(entry.value().clone());
|
||||
}
|
||||
}
|
||||
ListNetworkProps::DisabledOnly => {
|
||||
if !self.enabled_networks.contains(&id) {
|
||||
ret.push(entry.value().clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
async fn get_network_config(
|
||||
&self,
|
||||
_: AppHandle,
|
||||
network_inst_id: &str,
|
||||
) -> Result<Option<GUIConfig>, anyhow::Error> {
|
||||
let uuid = Uuid::parse_str(network_inst_id)?;
|
||||
Ok(self
|
||||
.network_configs
|
||||
.get(&uuid)
|
||||
.map(|entry| entry.value().clone()))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct GUIClientManager {
|
||||
pub(super) storage: GUIStorage,
|
||||
rpc_manager: BidirectRpcManager,
|
||||
}
|
||||
impl GUIClientManager {
|
||||
pub async fn new() -> Result<Self, anyhow::Error> {
|
||||
let mut connector = RingTunnelConnector::new(
|
||||
format!("ring://{}", RPC_RING_UUID.deref()).parse().unwrap(),
|
||||
);
|
||||
let tunnel = connector.connect().await?;
|
||||
let rpc_manager = BidirectRpcManager::new();
|
||||
rpc_manager.run_with_tunnel(tunnel);
|
||||
|
||||
Ok(Self {
|
||||
storage: GUIStorage::new(),
|
||||
rpc_manager,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_enabled_instances_with_tun_ids(&self) -> impl Iterator<Item = uuid::Uuid> + '_ {
|
||||
self.storage
|
||||
.network_configs
|
||||
.iter()
|
||||
.filter(|v| self.storage.enabled_networks.contains(v.key()))
|
||||
.filter(|v| !v.1.no_tun())
|
||||
.filter_map(|c| c.1.instance_id().parse::<uuid::Uuid>().ok())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub(super) async fn disable_instances_with_tun(
|
||||
&self,
|
||||
app: &AppHandle,
|
||||
) -> Result<(), easytier::rpc_service::remote_client::RemoteClientError<anyhow::Error>>
|
||||
{
|
||||
for inst_id in self.get_enabled_instances_with_tun_ids() {
|
||||
self.handle_update_network_state(app.clone(), inst_id, true)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn notify_vpn_stop_if_no_tun(&self, app: &AppHandle) -> Result<(), String> {
|
||||
let has_tun = self.get_enabled_instances_with_tun_ids().any(|_| true);
|
||||
if !has_tun {
|
||||
app.emit("vpn_service_stop", "")
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl RemoteClientManager<AppHandle, GUIConfig, anyhow::Error> for GUIClientManager {
|
||||
fn get_rpc_client(
|
||||
&self,
|
||||
_: AppHandle,
|
||||
) -> Option<Box<dyn WebClientService<Controller = BaseController> + Send>> {
|
||||
Some(
|
||||
self.rpc_manager
|
||||
.rpc_client()
|
||||
.scoped_client::<WebClientServiceClientFactory<BaseController>>(
|
||||
1,
|
||||
1,
|
||||
"".to_string(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn get_storage(&self) -> &impl Storage<AppHandle, GUIConfig, anyhow::Error> {
|
||||
&self.storage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
@@ -176,6 +556,24 @@ pub fn run() {
|
||||
|
||||
utils::setup_panic_handler();
|
||||
|
||||
let _rpc_server_handle = tauri::async_runtime::spawn(async move {
|
||||
let rpc_server = ApiRpcServer::from_tunnel(
|
||||
RingTunnelListener::new(format!("ring://{}", RPC_RING_UUID.deref()).parse().unwrap()),
|
||||
INSTANCE_MANAGER.clone(),
|
||||
)
|
||||
.serve()
|
||||
.await
|
||||
.expect("Failed to start RPC server");
|
||||
|
||||
let _ = CLIENT_MANAGER.set(
|
||||
manager::GUIClientManager::new()
|
||||
.await
|
||||
.expect("Failed to create GUI client manager"),
|
||||
);
|
||||
|
||||
rpc_server
|
||||
});
|
||||
|
||||
let mut builder = tauri::Builder::default();
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
@@ -257,14 +655,18 @@ pub fn run() {
|
||||
parse_network_config,
|
||||
generate_network_config,
|
||||
run_network_instance,
|
||||
retain_network_instance,
|
||||
collect_network_infos,
|
||||
get_os_hostname,
|
||||
collect_network_info,
|
||||
set_logging_level,
|
||||
set_tun_fd,
|
||||
is_autostart,
|
||||
easytier_version,
|
||||
set_dock_visibility
|
||||
set_dock_visibility,
|
||||
list_network_instance_ids,
|
||||
remove_network_instance,
|
||||
update_network_config_state,
|
||||
save_network_config,
|
||||
validate_config,
|
||||
get_config,
|
||||
load_configs,
|
||||
])
|
||||
.on_window_event(|_win, event| match event {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
|
||||
Reference in New Issue
Block a user