Ctrl K
ComunidadGuías…

src-tauri/src/lib.rs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
mod commands;
mod config;
#[cfg(desktop)]
mod connection;
#[cfg(mobile)]
#[path = "android/connection.rs"]
mod connection;
mod elevation;
mod export;
mod import;
mod models;
mod profiles;
mod settings;
mod stats;
mod storage;
mod subscriptions;
mod sysproxy;

use connection::Connection;
#[cfg(desktop)]
use tauri::menu::{Menu, MenuItem};
#[cfg(desktop)]
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
use tauri::{AppHandle, Manager};
#[cfg(desktop)]
use tauri_plugin_autostart::ManagerExt;

/// Аргумент, с которым автозапуск системы стартует приложение.
pub const AUTOSTART_ARG: &str = "--autostart";

/// Маркер «этот запуск пришёл из автозапуска» для редиректа через задачу
/// Планировщика (adminLaunch): задача запускает exe без аргументов, поэтому
/// обычный процесс перед редиректом оставляет файл-маркер
/// (см. elevation::admin_launch_startup).
pub const AUTOSTART_MARKER: &str = "autostart-redirect.flag";

/// Запущены ли мы автозапуском системы: по аргументу `--autostart` либо по
/// свежему маркеру от процесса, средиректившего нас через Планировщик.
/// Маркер одноразовый — удаляется при любом исходе.
#[cfg(desktop)]
fn launched_via_autostart(app: &AppHandle) -> bool {
    let by_arg = std::env::args().any(|a| a == AUTOSTART_ARG);
    let mut by_marker = false;
    if let Ok(marker) = storage::path(app, AUTOSTART_MARKER) {
        if marker.exists() {
            by_marker = std::fs::metadata(&marker)
                .and_then(|m| m.modified())
                .ok()
                .and_then(|t| t.elapsed().ok())
                .is_some_and(|age| age < std::time::Duration::from_secs(120));
            let _ = std::fs::remove_file(&marker);
        }
    }
    by_arg || by_marker
}

/// Перерегистрирует включённый автозапуск: записи, сделанные до появления
/// `--autostart`, не содержат аргумента — enable() идемпотентно докидывает его.
#[cfg(desktop)]
fn refresh_autostart(app: &AppHandle) {
    let autolaunch = app.autolaunch();
    if autolaunch.is_enabled().unwrap_or(false) {
        let _ = autolaunch.enable();
    }
}

/// Показывает и фокусирует главное окно (из трея).
#[cfg(desktop)]
fn show_main(app: &AppHandle) {
    if let Some(w) = app.get_webview_window("main") {
        let _ = w.show();
        let _ = w.unminimize();
        let _ = w.set_focus();
    }
}

/// Иконка в системном трее с меню (Показать / Скрыть / Выход).
#[cfg(desktop)]
fn tray_menu(app: &AppHandle, language: &str) -> tauri::Result<Menu<tauri::Wry>> {
    let english = language == "en";
    let show = MenuItem::with_id(app, "show", if english { "Show" } else { "Показать" }, true, None::<&str>)?;
    let hide = MenuItem::with_id(app, "hide", if english { "Hide to tray" } else { "Скрыть в трей" }, true, None::<&str>)?;
    let quit = MenuItem::with_id(app, "quit", if english { "Quit" } else { "Выход" }, true, None::<&str>)?;
    Menu::with_items(app, &[&show, &hide, &quit])
}

#[cfg(desktop)]
pub(crate) fn refresh_tray_language(app: &AppHandle, language: &str) -> tauri::Result<()> {
    if let Some(tray) = app.tray_by_id("main") {
        tray.set_menu(Some(tray_menu(app, language)?))?;
    }
    Ok(())
}

#[cfg(desktop)]
fn build_tray(app: &AppHandle) -> tauri::Result<()> {
    let language = settings::load(app).map(|settings| settings.language).unwrap_or_else(|_| "ru".into());
    let menu = tray_menu(app, &language)?;

    TrayIconBuilder::with_id("main")
        .icon(app.default_window_icon().unwrap().clone())
        .tooltip("UniGate")
        .menu(&menu)
        .show_menu_on_left_click(false)
        .on_menu_event(|app, event| match event.id.as_ref() {
            "show" => show_main(app),
            "hide" => {
                if let Some(w) = app.get_webview_window("main") {
                    let _ = w.hide();
                }
            }
            "quit" => app.exit(0),
            _ => {}
        })
        .on_tray_icon_event(|tray, event| {
            if let TrayIconEvent::Click {
                button: MouseButton::Left,
                button_state: MouseButtonState::Up,
                ..
            } = event
            {
                show_main(tray.app_handle());
            }
        })
        .build(app)?;
    Ok(())
}

/// Закрытие окна: если включено «сворачивать в трей» — прячем, а не выходим.
#[cfg(desktop)]
fn on_window_event(window: &tauri::Window, event: &tauri::WindowEvent) {
    if let tauri::WindowEvent::CloseRequested { api, .. } = event {
        let to_tray = settings::load(window.app_handle())
            .map(|s| s.minimize_to_tray)
            .unwrap_or(true);
        if to_tray {
            api.prevent_close();
            let _ = window.hide();
        }
    }
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    let builder = tauri::Builder::default()
        .plugin(tauri_plugin_opener::init())
        .plugin(tauri_plugin_shell::init())
        .plugin(tauri_plugin_dialog::init());
    #[cfg(windows)]
    let builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
    #[cfg(desktop)]
    let builder = builder.plugin(tauri_plugin_autostart::init(
        tauri_plugin_autostart::MacosLauncher::LaunchAgent,
        Some(vec![AUTOSTART_ARG]),
    ));
    #[cfg(mobile)]
    let builder = builder.plugin(connection::init());
    let builder = builder.manage(Connection::new()).setup(|app| {
        // Windows + «запуск от администратора»: обычный запуск редиректим
        // через задачу планировщика (элевация без UAC) и выходим
        #[cfg(windows)]
        if elevation::admin_launch_startup(app.handle()) {
            app.handle().exit(0);
            return Ok(());
        }
        #[cfg(desktop)]
        {
            // окно создаётся скрытым (tauri.conf.json): при запуске
            // автозапуском системы остаёмся в трее, иначе показываем
            if !launched_via_autostart(app.handle()) {
                show_main(app.handle());
            }
            refresh_autostart(app.handle());
            // снять «зависший» системный прокси после нештатного выхода
            connection::reconcile_startup(app.handle());
            build_tray(app.handle())?;
        }
        #[cfg(mobile)]
        let _ = app;
        Ok(())
    });
    #[cfg(desktop)]
    let builder = builder.on_window_event(on_window_event);
    builder
        .invoke_handler(tauri::generate_handler![
            commands::singbox_version,
            commands::get_settings,
            commands::save_settings,
            commands::list_profiles,
            commands::create_profile,
            commands::update_profile,
            commands::delete_profile,
            commands::duplicate_profile,
            commands::import_profile,
            commands::export_profile,
            commands::list_subscriptions,
            commands::add_subscription,
            commands::update_subscription,
            commands::delete_subscription,
            commands::list_installed_apps,
            commands::request_android_quick_tile,
            commands::request_android_widget,
            connection::connect,
            connection::disconnect,
            connection::get_connection_state,
            connection::local_proxy_addr,
            connection::awg_shim_available,
            elevation::is_elevated_cmd,
            elevation::relaunch_elevated_cmd,
            elevation::apply_admin_launch,
        ])
        .build(tauri::generate_context!())
        .expect("error while building tauri application")
        .run(|app, event| {
            // при выходе гарантированно убиваем sing-box и снимаем системный прокси
            #[cfg(desktop)]
            if let tauri::RunEvent::Exit = event {
                connection::cleanup(app);
            }
            #[cfg(mobile)]
            let _ = (app, event);
        });
}