mirror of
https://github.com/by-sonic/tglock.git
synced 2026-11-05 15:21:55 +03:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bc18509b9c | |||
| 098178ead4 | |||
| 7b48f0046b |
Generated
+1
-1
@@ -3822,7 +3822,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tglock"
|
||||
version = "2.0.0-beta.17"
|
||||
version = "2.0.0-beta.18"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"cipher",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tglock"
|
||||
version = "2.0.0-beta.17"
|
||||
version = "2.0.0-beta.18"
|
||||
edition = "2021"
|
||||
rust-version = "1.88"
|
||||
description = "Telegram unblock via local WebSocket tunnel"
|
||||
@@ -38,7 +38,7 @@ path = "src/bin/cli.rs"
|
||||
required-features = ["cli"]
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [], optional = true }
|
||||
tauri = { version = "2", features = ["tray-icon"], optional = true }
|
||||
tauri-plugin-opener = { version = "2", optional = true }
|
||||
clap = { version = "4", features = ["derive"], optional = true }
|
||||
# Файл конфигурации CLI. TOML, а не JSON, потому что файл правят руками и в нём
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
<!-- SUPPORT-START -->
|
||||
<p align="center">
|
||||
<b>💛 Поддержать проект</b> · <b>USDT</b> (Tether) в сети <b>TRC20</b>:<br/>
|
||||
<code>TEHHyPSeZ7t5vfVJmjtEBkqJdpfcrm1wqg</code>
|
||||
</p>
|
||||
<!-- SUPPORT-END -->
|
||||
|
||||
<!-- ════════════════════════ ROSEVPN — sponsor ════════════════════════ -->
|
||||
|
||||
<p align="center">
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "tglock-ui",
|
||||
"version": "2.0.0-beta.17",
|
||||
"version": "2.0.0-beta.18",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "tglock-ui",
|
||||
"version": "2.0.0-beta.17",
|
||||
"version": "2.0.0-beta.18",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "tglock-ui",
|
||||
"private": true,
|
||||
"version": "2.0.0-beta.17",
|
||||
"version": "2.0.0-beta.18",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test:worker": "node --experimental-vm-modules --test worker/tglock-worker.test.js",
|
||||
|
||||
+106
-2
@@ -431,8 +431,19 @@ pub fn run() {
|
||||
);
|
||||
}
|
||||
app.manage(state);
|
||||
#[cfg(desktop)]
|
||||
if let Err(error) = setup_tray(app) {
|
||||
app.state::<AppState>().log(
|
||||
format!("Значок в трее недоступен ({error}). Закрытие окна выключит защиту"),
|
||||
true,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|_window, _event| {
|
||||
#[cfg(desktop)]
|
||||
hide_to_tray_on_close(_window, _event);
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_status,
|
||||
get_settings,
|
||||
@@ -442,8 +453,101 @@ pub fn run() {
|
||||
get_telegram_link,
|
||||
open_telegram
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("failed to run TGLock");
|
||||
.build(tauri::generate_context!())
|
||||
.expect("failed to build TGLock")
|
||||
.run(|_app, _event| {
|
||||
// В macOS окно, спрятанное в трей, возвращает и клик по значку в Dock.
|
||||
#[cfg(target_os = "macos")]
|
||||
if let tauri::RunEvent::Reopen { .. } = _event {
|
||||
show_main_window(_app);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
const TRAY_ID: &str = "main";
|
||||
|
||||
/// Закрытие окна при включённой защите прячет его в трей, и прокси продолжает
|
||||
/// работать, как в tg-ws-proxy. Выйти можно из меню трея (by-sonic/tglock#70).
|
||||
/// Если защита выключена или значка в трее нет, окно закрывается как раньше:
|
||||
/// иначе приложение осталось бы работать без способа до него добраться.
|
||||
#[cfg(desktop)]
|
||||
fn hide_to_tray_on_close(window: &tauri::Window, event: &tauri::WindowEvent) {
|
||||
let tauri::WindowEvent::CloseRequested { api, .. } = event else {
|
||||
return;
|
||||
};
|
||||
let app = window.app_handle();
|
||||
let running = app.state::<AppState>().stats.running.load(Ordering::SeqCst);
|
||||
if running && app.tray_by_id(TRAY_ID).is_some() {
|
||||
api.prevent_close();
|
||||
let _ = window.hide();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
fn show_main_window(app: &tauri::AppHandle) {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.unminimize();
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
fn setup_tray(app: &tauri::App) -> Result<(), String> {
|
||||
// В Linux значок держится на libayatana-appindicator, и без неё tray-icon
|
||||
// паникует, а не возвращает ошибку. Без трея окно просто закрывается.
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| build_tray(app)))
|
||||
.map_err(|_| "нет libayatana-appindicator3".to_string())?
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
fn build_tray(app: &tauri::App) -> tauri::Result<()> {
|
||||
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
|
||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
||||
|
||||
let show = MenuItem::with_id(app, "show", "Открыть TGLock", true, None::<&str>)?;
|
||||
let telegram = MenuItem::with_id(app, "telegram", "Открыть Telegram", true, None::<&str>)?;
|
||||
let separator = PredefinedMenuItem::separator(app)?;
|
||||
let quit = MenuItem::with_id(app, "quit", "Выход", true, None::<&str>)?;
|
||||
let menu = Menu::with_items(app, &[&show, &telegram, &separator, &quit])?;
|
||||
|
||||
let mut tray = TrayIconBuilder::with_id(TRAY_ID)
|
||||
.tooltip("TGLock")
|
||||
.menu(&menu)
|
||||
// В Windows и Linux левый клик открывает окно, меню — по правому.
|
||||
// В macOS меню по клику привычнее.
|
||||
.show_menu_on_left_click(cfg!(target_os = "macos"))
|
||||
.on_menu_event(|app, event| match event.id().as_ref() {
|
||||
"show" => show_main_window(app),
|
||||
"telegram" => {
|
||||
let state = app.state::<AppState>();
|
||||
if let Err(error) = open_telegram_link(app, &state) {
|
||||
state.log(error, true);
|
||||
}
|
||||
}
|
||||
"quit" => app.exit(0),
|
||||
_ => {}
|
||||
})
|
||||
.on_tray_icon_event(|tray, event| {
|
||||
if cfg!(target_os = "macos") {
|
||||
return;
|
||||
}
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
button_state: MouseButtonState::Up,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
show_main_window(tray.app_handle());
|
||||
}
|
||||
});
|
||||
if let Some(icon) = app.default_window_icon() {
|
||||
tray = tray.icon(icon.clone());
|
||||
}
|
||||
tray.build(app)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "TGLock",
|
||||
"version": "2.0.0-beta.17",
|
||||
"version": "2.0.0-beta.18",
|
||||
"identifier": "com.bysonic.tglock",
|
||||
"mainBinaryName": "tglock",
|
||||
"build": {
|
||||
|
||||
+70
-20
@@ -113,6 +113,47 @@ function connectionState(): { title: string; subtitle: string; className: string
|
||||
};
|
||||
}
|
||||
|
||||
// Статус приходит раз в секунду. Если каждый раз переписывать root.innerHTML,
|
||||
// кнопки становятся новыми узлами, и тап, начатый на старой кнопке и
|
||||
// законченный на новой, Chromium не превращает в click. Поэтому разметка
|
||||
// накладывается на уже существующие узлы: меняются только текст и атрибуты,
|
||||
// а элемент заменяется лишь там, где на его месте теперь другой тег или id.
|
||||
function patchRoot(html: string): void {
|
||||
const next = document.createElement("template");
|
||||
next.innerHTML = html;
|
||||
patchChildren(root, next.content);
|
||||
}
|
||||
|
||||
function patchChildren(current: Node, next: Node): void {
|
||||
const nextChildren = Array.from(next.childNodes);
|
||||
nextChildren.forEach((nextChild, index) => {
|
||||
const currentChild = current.childNodes[index];
|
||||
if (!currentChild) current.appendChild(nextChild);
|
||||
else if (!sameSlot(currentChild, nextChild)) current.replaceChild(nextChild, currentChild);
|
||||
else patchNode(currentChild, nextChild);
|
||||
});
|
||||
while (current.childNodes.length > nextChildren.length) current.lastChild?.remove();
|
||||
}
|
||||
|
||||
function sameSlot(current: Node, next: Node): boolean {
|
||||
if (current.nodeType !== next.nodeType || current.nodeName !== next.nodeName) return false;
|
||||
return !(current instanceof Element && next instanceof Element) || current.id === next.id;
|
||||
}
|
||||
|
||||
function patchNode(current: Node, next: Node): void {
|
||||
if (!(current instanceof Element && next instanceof Element)) {
|
||||
if (current.nodeValue !== next.nodeValue) current.nodeValue = next.nodeValue;
|
||||
return;
|
||||
}
|
||||
for (const { name } of Array.from(current.attributes)) {
|
||||
if (!next.hasAttribute(name)) current.removeAttribute(name);
|
||||
}
|
||||
for (const { name, value } of Array.from(next.attributes)) {
|
||||
if (current.getAttribute(name) !== value) current.setAttribute(name, value);
|
||||
}
|
||||
patchChildren(current, next);
|
||||
}
|
||||
|
||||
function shell(content: string, pageClass = ""): string {
|
||||
return `
|
||||
<main class="app-shell ${pageClass}">
|
||||
@@ -125,7 +166,7 @@ function shell(content: string, pageClass = ""): string {
|
||||
|
||||
function renderHome(): void {
|
||||
const connection = connectionState();
|
||||
root.innerHTML = shell(`
|
||||
patchRoot(shell(`
|
||||
<section class="home">
|
||||
<header class="brand">
|
||||
<div class="wordmark"><span>TG</span>Lock</div>
|
||||
@@ -191,14 +232,7 @@ function renderHome(): void {
|
||||
</button>
|
||||
</nav>
|
||||
</section>
|
||||
`, "home-page");
|
||||
|
||||
document.querySelector("#power")?.addEventListener("click", toggleProtection);
|
||||
document.querySelector("#open-telegram")?.addEventListener("click", openTelegram);
|
||||
document.querySelector("#copy-link")?.addEventListener("click", copyTelegramLink);
|
||||
document.querySelector("#copy-address")?.addEventListener("click", copyShareAddress);
|
||||
document.querySelector("#settings-nav")?.addEventListener("click", () => navigate("settings"));
|
||||
document.querySelector("#diagnostics-nav")?.addEventListener("click", () => navigate("diagnostics"));
|
||||
`, "home-page"));
|
||||
}
|
||||
|
||||
function pageHeader(title: string, subtitle: string): string {
|
||||
@@ -214,7 +248,7 @@ function pageHeader(title: string, subtitle: string): string {
|
||||
}
|
||||
|
||||
function renderSettings(): void {
|
||||
root.innerHTML = shell(`
|
||||
patchRoot(shell(`
|
||||
<section class="subpage">
|
||||
${pageHeader("Настройки", "Подключение и локальная сеть")}
|
||||
|
||||
@@ -275,15 +309,12 @@ function renderSettings(): void {
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
`, "subpage-shell");
|
||||
|
||||
document.querySelector("#back")?.addEventListener("click", () => navigate("home"));
|
||||
document.querySelector("#settings-form")?.addEventListener("submit", saveSettings);
|
||||
`, "subpage-shell"));
|
||||
}
|
||||
|
||||
function renderDiagnostics(): void {
|
||||
const recentLogs = status.logs.slice(-4).reverse();
|
||||
root.innerHTML = shell(`
|
||||
patchRoot(shell(`
|
||||
<section class="subpage">
|
||||
${pageHeader("Диагностика", "Состояние маршрута в реальном времени")}
|
||||
|
||||
@@ -372,12 +403,31 @@ function renderDiagnostics(): void {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`, "subpage-shell");
|
||||
|
||||
document.querySelector("#copy-diagnostics")?.addEventListener("click", copyDiagnostics);
|
||||
document.querySelector("#back")?.addEventListener("click", () => navigate("home"));
|
||||
`, "subpage-shell"));
|
||||
}
|
||||
|
||||
// Обработчики висят на #app, а не на кнопках: patchRoot сохраняет кнопки,
|
||||
// но новые (например, «Открыть Telegram» после включения) создаёт сам.
|
||||
const clickActions: Record<string, () => void> = {
|
||||
power: toggleProtection,
|
||||
"open-telegram": openTelegram,
|
||||
"copy-link": copyTelegramLink,
|
||||
"copy-address": copyShareAddress,
|
||||
"settings-nav": () => navigate("settings"),
|
||||
"diagnostics-nav": () => navigate("diagnostics"),
|
||||
"copy-diagnostics": copyDiagnostics,
|
||||
back: () => navigate("home"),
|
||||
};
|
||||
|
||||
root.addEventListener("click", (event) => {
|
||||
const button = (event.target as Element).closest<HTMLButtonElement>("button[id]");
|
||||
if (button && !button.disabled) clickActions[button.id]?.();
|
||||
});
|
||||
|
||||
root.addEventListener("submit", (event) => {
|
||||
if ((event.target as Element).id === "settings-form") void saveSettings(event);
|
||||
});
|
||||
|
||||
function render(): void {
|
||||
if (view === "settings") renderSettings();
|
||||
else if (view === "diagnostics") renderDiagnostics();
|
||||
@@ -468,7 +518,7 @@ async function copyDiagnostics(): Promise<void> {
|
||||
// Public reports exclude proxy links, worker domains and free-form logs.
|
||||
const report = {
|
||||
application: "TGLock",
|
||||
version: "2.0.0-beta.17",
|
||||
version: "2.0.0-beta.18",
|
||||
running: status.running,
|
||||
activeConnections: status.activeConnections,
|
||||
tunnels: status.tunnels,
|
||||
|
||||
Reference in New Issue
Block a user