mirror of
https://github.com/by-sonic/tglock.git
synced 2026-08-11 00:33:21 +03:00
feat(gui): показывать адрес для других устройств в LAN-режиме (#40)
Второй человек за неделю не смог подключить телефон, потому что адрес негде взять. В #36 @modx-arseniy вписал в Telegram на Android настройки «как в клиентах на винде» — то есть 127.0.0.1, который на другом устройстве означает само это устройство, а не компьютер с прокси. Раньше в README было написано, что адрес показывается в интерфейсе; это оказалось неправдой, и я тогда исправил README вместо приложения. Теперь исправлено приложение. В LAN-режиме на главном экране появляется карточка с адресом вида 192.168.1.7:1080. Нажатие копирует его в буфер. Под адресом — предупреждение про 127.0.0.1, потому что именно туда люди и уходят. Адрес берётся из работающего слушателя, а не из текущих настроек: если человек поменял порт, но не перезапустил прокси, показать надо тот, на котором прокси реально поднят. Логика вынесена в чистую функцию share_address и покрыта тестами: на выключенном прокси и на loopback делиться нечем, в LAN-режиме адрес обязан содержать порт и не быть ни 0.0.0.0, ни 127.0.0.1. Co-authored-by: by-sonic <171230345+by-sonic@users.noreply.github.com>
This commit is contained in:
+57
@@ -49,6 +49,13 @@ struct StatusSnapshot {
|
|||||||
route_failures: u32,
|
route_failures: u32,
|
||||||
uptime_seconds: u64,
|
uptime_seconds: u64,
|
||||||
port: u16,
|
port: u16,
|
||||||
|
/// Адрес, который нужно вписать в Telegram на другом устройстве.
|
||||||
|
///
|
||||||
|
/// В LAN-режиме это адрес этого компьютера в локальной сети. Люди искали
|
||||||
|
/// его в интерфейсе и не находили: вписывали `127.0.0.1`, который на
|
||||||
|
/// телефоне или в эмуляторе означает само устройство, и подключение не
|
||||||
|
/// работало (by-sonic/tglock#36).
|
||||||
|
share_address: Option<String>,
|
||||||
logs: Vec<LogLine>,
|
logs: Vec<LogLine>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +63,10 @@ struct AppState {
|
|||||||
stats: Arc<proxy::Stats>,
|
stats: Arc<proxy::Stats>,
|
||||||
settings: Mutex<Settings>,
|
settings: Mutex<Settings>,
|
||||||
active_port: Mutex<u16>,
|
active_port: Mutex<u16>,
|
||||||
|
/// Слушатель работающего прокси. Нужен, чтобы показать адрес для других
|
||||||
|
/// устройств именно тот, на котором прокси реально поднят, а не тот, что
|
||||||
|
/// сейчас выбран в настройках.
|
||||||
|
active_listen: Mutex<Option<ListenConfig>>,
|
||||||
started_at: Mutex<Option<Instant>>,
|
started_at: Mutex<Option<Instant>>,
|
||||||
logs: Arc<Mutex<Vec<LogLine>>>,
|
logs: Arc<Mutex<Vec<LogLine>>>,
|
||||||
settings_path: PathBuf,
|
settings_path: PathBuf,
|
||||||
@@ -71,6 +82,7 @@ impl AppState {
|
|||||||
stats: proxy::Stats::new(),
|
stats: proxy::Stats::new(),
|
||||||
settings: Mutex::new(settings),
|
settings: Mutex::new(settings),
|
||||||
active_port: Mutex::new(proxy::DEFAULT_PORT),
|
active_port: Mutex::new(proxy::DEFAULT_PORT),
|
||||||
|
active_listen: Mutex::new(None),
|
||||||
started_at: Mutex::new(None),
|
started_at: Mutex::new(None),
|
||||||
logs: Arc::new(Mutex::new(Vec::new())),
|
logs: Arc::new(Mutex::new(Vec::new())),
|
||||||
settings_path,
|
settings_path,
|
||||||
@@ -106,6 +118,7 @@ impl AppState {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.map_or(0, |started| started.elapsed().as_secs()),
|
.map_or(0, |started| started.elapsed().as_secs()),
|
||||||
port: *self.active_port.lock().unwrap(),
|
port: *self.active_port.lock().unwrap(),
|
||||||
|
share_address: share_address(*self.active_listen.lock().unwrap()),
|
||||||
logs: self.logs.lock().unwrap().clone(),
|
logs: self.logs.lock().unwrap().clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,6 +135,18 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Адрес, который нужно вписать в Telegram на другом устройстве.
|
||||||
|
///
|
||||||
|
/// Только для слушателя на `0.0.0.0`: на loopback делиться нечем, туда никто
|
||||||
|
/// извне не достучится. Возвращается адрес этой машины в сети, а не `0.0.0.0`
|
||||||
|
/// и не `127.0.0.1` — последний на телефоне или в эмуляторе означает само
|
||||||
|
/// устройство, и именно на этом спотыкались (by-sonic/tglock#36).
|
||||||
|
fn share_address(listen: Option<ListenConfig>) -> Option<String> {
|
||||||
|
listen
|
||||||
|
.filter(|listen| listen.addr.ip().is_unspecified())
|
||||||
|
.map(|listen| format!("{}:{}", listen.advertised_host(), listen.addr.port()))
|
||||||
|
}
|
||||||
|
|
||||||
fn current_time() -> String {
|
fn current_time() -> String {
|
||||||
let seconds = SystemTime::now()
|
let seconds = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
@@ -177,6 +202,8 @@ fn start_proxy(state: State<'_, AppState>) -> Result<StatusSnapshot, String> {
|
|||||||
ListenConfig::loopback(settings.port)
|
ListenConfig::loopback(settings.port)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
*state.active_listen.lock().unwrap() = Some(listen);
|
||||||
|
|
||||||
let stats = state.stats.clone();
|
let stats = state.stats.clone();
|
||||||
let logs = state.logs.clone();
|
let logs = state.logs.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
@@ -194,6 +221,7 @@ fn start_proxy(state: State<'_, AppState>) -> Result<StatusSnapshot, String> {
|
|||||||
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(220));
|
std::thread::sleep(std::time::Duration::from_millis(220));
|
||||||
if !state.stats.running.load(Ordering::SeqCst) {
|
if !state.stats.running.load(Ordering::SeqCst) {
|
||||||
|
*state.active_listen.lock().unwrap() = None;
|
||||||
*state.started_at.lock().unwrap() = None;
|
*state.started_at.lock().unwrap() = None;
|
||||||
return Err(state
|
return Err(state
|
||||||
.logs
|
.logs
|
||||||
@@ -213,6 +241,7 @@ fn start_proxy(state: State<'_, AppState>) -> Result<StatusSnapshot, String> {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn stop_proxy(state: State<'_, AppState>) -> StatusSnapshot {
|
fn stop_proxy(state: State<'_, AppState>) -> StatusSnapshot {
|
||||||
state.stats.stop();
|
state.stats.stop();
|
||||||
|
*state.active_listen.lock().unwrap() = None;
|
||||||
*state.started_at.lock().unwrap() = None;
|
*state.started_at.lock().unwrap() = None;
|
||||||
state.log("Защита выключена", false);
|
state.log("Защита выключена", false);
|
||||||
state.snapshot()
|
state.snapshot()
|
||||||
@@ -311,6 +340,34 @@ fn main() {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nothing_to_share_when_the_proxy_is_off_or_local() {
|
||||||
|
assert_eq!(
|
||||||
|
share_address(None),
|
||||||
|
None,
|
||||||
|
"выключенный прокси нечего делить"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
share_address(Some(ListenConfig::loopback(1080))),
|
||||||
|
None,
|
||||||
|
"на loopback снаружи никто не подключится"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lan_mode_shares_a_reachable_address() {
|
||||||
|
let shown = share_address(Some(ListenConfig::lan(1443))).expect("в LAN-режиме адрес нужен");
|
||||||
|
assert!(shown.ends_with(":1443"), "порт должен быть виден: {shown}");
|
||||||
|
assert!(
|
||||||
|
!shown.starts_with("0.0.0.0"),
|
||||||
|
"0.0.0.0 нельзя вписать в Telegram: {shown}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!shown.starts_with("127.0.0.1"),
|
||||||
|
"127.0.0.1 на другом устройстве означает само устройство: {shown}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn software_rendering_is_requested_by_default() {
|
fn software_rendering_is_requested_by_default() {
|
||||||
let vars = software_rendering_vars(false, |_| false);
|
let vars = software_rendering_vars(false, |_| false);
|
||||||
|
|||||||
+28
@@ -13,6 +13,8 @@ type Status = {
|
|||||||
routeFailures: number;
|
routeFailures: number;
|
||||||
uptimeSeconds: number;
|
uptimeSeconds: number;
|
||||||
port: number;
|
port: number;
|
||||||
|
/// Адрес для других устройств. Приходит только в LAN-режиме.
|
||||||
|
shareAddress: string | null;
|
||||||
logs: LogLine[];
|
logs: LogLine[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -41,6 +43,7 @@ let status: Status = {
|
|||||||
routeFailures: 0,
|
routeFailures: 0,
|
||||||
uptimeSeconds: 0,
|
uptimeSeconds: 0,
|
||||||
port: 1080,
|
port: 1080,
|
||||||
|
shareAddress: null,
|
||||||
logs: [],
|
logs: [],
|
||||||
};
|
};
|
||||||
let settings: Settings = { lanMode: false, port: 1080, workerDomain: "" };
|
let settings: Settings = { lanMode: false, port: 1080, workerDomain: "" };
|
||||||
@@ -147,6 +150,18 @@ function renderHome(): void {
|
|||||||
<span>порт ${status.port}</span>
|
<span>порт ${status.port}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
${status.shareAddress ? `
|
||||||
|
<div class="share-card">
|
||||||
|
<div class="share-label">Адрес для других устройств</div>
|
||||||
|
<button id="copy-address" class="share-address" title="Нажмите, чтобы скопировать">
|
||||||
|
${escapeHtml(status.shareAddress)}
|
||||||
|
</button>
|
||||||
|
<div class="share-hint">
|
||||||
|
Впишите его в Telegram на телефоне. Не <code>127.0.0.1</code> — на другом
|
||||||
|
устройстве это означает само устройство.
|
||||||
|
</div>
|
||||||
|
</div>` : ""}
|
||||||
|
|
||||||
<nav class="bottom-nav" aria-label="Разделы">
|
<nav class="bottom-nav" aria-label="Разделы">
|
||||||
<button id="settings-nav" class="nav-button">
|
<button id="settings-nav" class="nav-button">
|
||||||
<span class="nav-icon">${icons.settings}</span>
|
<span class="nav-icon">${icons.settings}</span>
|
||||||
@@ -162,6 +177,7 @@ function renderHome(): void {
|
|||||||
`, "home-page");
|
`, "home-page");
|
||||||
|
|
||||||
document.querySelector("#power")?.addEventListener("click", toggleProtection);
|
document.querySelector("#power")?.addEventListener("click", toggleProtection);
|
||||||
|
document.querySelector("#copy-address")?.addEventListener("click", copyShareAddress);
|
||||||
document.querySelector("#settings-nav")?.addEventListener("click", () => navigate("settings"));
|
document.querySelector("#settings-nav")?.addEventListener("click", () => navigate("settings"));
|
||||||
document.querySelector("#diagnostics-nav")?.addEventListener("click", () => navigate("diagnostics"));
|
document.querySelector("#diagnostics-nav")?.addEventListener("click", () => navigate("diagnostics"));
|
||||||
}
|
}
|
||||||
@@ -368,6 +384,18 @@ async function saveSettings(event: Event): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function copyShareAddress(): Promise<void> {
|
||||||
|
const address = status.shareAddress;
|
||||||
|
if (!address) return;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(address);
|
||||||
|
showToast("Адрес скопирован");
|
||||||
|
} catch {
|
||||||
|
// Буфер обмена может быть недоступен — адрес и так виден на экране.
|
||||||
|
showToast("Скопируйте адрес вручную", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showToast(message: string, error = false): void {
|
function showToast(message: string, error = false): void {
|
||||||
window.clearTimeout(toastTimer);
|
window.clearTimeout(toastTimer);
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
|
|||||||
@@ -847,3 +847,53 @@ input:disabled {
|
|||||||
0 19px 52px rgba(9, 182, 133, 0.4);
|
0 19px 52px rgba(9, 182, 133, 0.4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Адрес для подключения других устройств. Показывается только в LAN-режиме:
|
||||||
|
люди искали его в интерфейсе, не находили и вписывали 127.0.0.1, который на
|
||||||
|
телефоне означает сам телефон (by-sonic/tglock#36). */
|
||||||
|
.share-card {
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(42, 171, 238, 0.08);
|
||||||
|
border: 1px solid rgba(42, 171, 238, 0.24);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-label {
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
color: var(--text-muted, #8b949e);
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-address {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin: 6px 0 8px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--accent, #2aabee);
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-address:hover,
|
||||||
|
.share-address:focus-visible {
|
||||||
|
background: rgba(42, 171, 238, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-hint {
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: var(--text-muted, #8b949e);
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-hint code {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user