feature: added advanced mode & added network mode & some UI improvements
This commit is contained in:
parent
72e5040e6d
commit
cf3ab75754
|
|
@ -93,6 +93,9 @@ Tray-приложение хранит данные в `%APPDATA%/TgWsProxy`:
|
|||
```json
|
||||
{
|
||||
"port": 1080,
|
||||
"host": "127.0.0.1",
|
||||
"listen_all": false,
|
||||
"advanced_host": false,
|
||||
"dc_ip": [
|
||||
"2:149.154.167.220",
|
||||
"4:149.154.167.220"
|
||||
|
|
@ -101,6 +104,10 @@ Tray-приложение хранит данные в `%APPDATA%/TgWsProxy`:
|
|||
}
|
||||
```
|
||||
|
||||
### Режим «Вся локальная сеть»
|
||||
|
||||
В **Настройки** можно включить режим **«Вся локальная сеть»**: прокси будет слушать на `0.0.0.0` и станет доступен с других устройств в той же сети. В окне настроек и в меню трея отображается **адрес для подключения** (IP вашего компьютера в сети и порт). На других устройствах укажите этот адрес в настройках прокси Telegram (SOCKS5). Убедитесь, что брандмауэр Windows разрешает входящие подключения на выбранный порт.
|
||||
|
||||
## Автоматическая сборка
|
||||
|
||||
Проект содержит спецификацию PyInstaller ([`windows.spec`](packaging/windows.spec)) и GitHub Actions workflow ([`.github/workflows/build.yml`](.github/workflows/build.yml)) для автоматической сборки.
|
||||
|
|
|
|||
395
windows.py
395
windows.py
|
|
@ -5,6 +5,7 @@ import json
|
|||
import logging
|
||||
import os
|
||||
import psutil
|
||||
import socket as _socket
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
|
@ -30,11 +31,96 @@ FIRST_RUN_MARKER = APP_DIR / ".first_run_done"
|
|||
DEFAULT_CONFIG = {
|
||||
"port": 1080,
|
||||
"host": "127.0.0.1",
|
||||
"listen_all": False,
|
||||
"advanced_host": False,
|
||||
"dc_ip": ["2:149.154.167.220", "4:149.154.167.220"],
|
||||
"verbose": False,
|
||||
}
|
||||
|
||||
|
||||
def _get_all_local_ips() -> list[str]:
|
||||
result: list[str] = []
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
import subprocess
|
||||
out = subprocess.run(
|
||||
["ipconfig", "/all"],
|
||||
capture_output=True, text=True, timeout=5, encoding="utf-8", errors="replace"
|
||||
)
|
||||
if out.returncode != 0:
|
||||
raise RuntimeError("ipconfig failed")
|
||||
for line in out.stdout.splitlines():
|
||||
line = line.strip()
|
||||
if "IPv4" in line and ":" in line:
|
||||
# " IPv4 Address. . . . . . . . . . . : 192.168.3.1(Preferred)"
|
||||
part = line.split(":", 1)[-1].strip().split()[0].strip().split("(")[0].strip()
|
||||
if part and part not in result:
|
||||
try:
|
||||
_socket.inet_aton(part)
|
||||
if part != "0.0.0.0":
|
||||
result.append(part)
|
||||
except OSError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
if not result:
|
||||
try:
|
||||
with _socket.socket(_socket.AF_INET, _socket.SOCK_DGRAM) as s:
|
||||
s.settimeout(0.5)
|
||||
s.connect(("8.8.8.8", 80))
|
||||
result.append(s.getsockname()[0])
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
|
||||
|
||||
def _get_lan_ip() -> str:
|
||||
"""Return LAN IP for display when listen_all is enabled. Prefers 192.168.* and avoids typical VPN ranges (10.8.*)."""
|
||||
ips = _get_all_local_ips()
|
||||
if not ips:
|
||||
return "?"
|
||||
# Priority: 192.168.* > other 10.* (except 10.8.* which is commonly VPN) > 10.8.* > others
|
||||
def rank(ip: str) -> tuple[int, str]:
|
||||
try:
|
||||
parts = ip.split(".")
|
||||
if len(parts) != 4:
|
||||
return (99, ip)
|
||||
a, b = int(parts[0]), int(parts[1])
|
||||
if a == 192 and b == 168:
|
||||
return (0, ip)
|
||||
if a == 10:
|
||||
if b == 8:
|
||||
return (3, ip)
|
||||
return (1, ip)
|
||||
return (2, ip)
|
||||
except (ValueError, IndexError):
|
||||
return (99, ip)
|
||||
ips.sort(key=rank)
|
||||
return ips[0]
|
||||
|
||||
|
||||
def _get_port(cfg: dict) -> int:
|
||||
return cfg.get("port", DEFAULT_CONFIG["port"]) if cfg.get("advanced_host", False) else DEFAULT_CONFIG["port"]
|
||||
|
||||
|
||||
def get_local_connection_address(cfg: dict) -> tuple[str, int]:
|
||||
return "127.0.0.1", _get_port(cfg)
|
||||
|
||||
|
||||
def is_network_available(cfg: dict) -> bool:
|
||||
if cfg.get("advanced_host", False):
|
||||
host = (cfg.get("host", DEFAULT_CONFIG["host"]) or "").strip()
|
||||
return host == "0.0.0.0"
|
||||
return bool(cfg.get("listen_all", False))
|
||||
|
||||
|
||||
def get_network_connection_address(cfg: dict) -> tuple[str, int]:
|
||||
port = _get_port(cfg)
|
||||
if not is_network_available(cfg):
|
||||
return "—", port
|
||||
return _get_lan_ip(), port
|
||||
|
||||
|
||||
_proxy_thread: Optional[threading.Thread] = None
|
||||
_async_stop: Optional[object] = None
|
||||
_tray_icon: Optional[object] = None
|
||||
|
|
@ -224,9 +310,14 @@ def start_proxy():
|
|||
return
|
||||
|
||||
cfg = _config
|
||||
port = cfg.get("port", DEFAULT_CONFIG["port"])
|
||||
if cfg.get("advanced_host", False):
|
||||
host = cfg.get("host", DEFAULT_CONFIG["host"])
|
||||
port = cfg.get("port", DEFAULT_CONFIG["port"])
|
||||
dc_ip_list = cfg.get("dc_ip", DEFAULT_CONFIG["dc_ip"])
|
||||
else:
|
||||
host = "0.0.0.0" if cfg.get("listen_all", False) else "127.0.0.1"
|
||||
port = DEFAULT_CONFIG["port"]
|
||||
dc_ip_list = DEFAULT_CONFIG["dc_ip"]
|
||||
verbose = cfg.get("verbose", False)
|
||||
|
||||
try:
|
||||
|
|
@ -260,6 +351,8 @@ def restart_proxy():
|
|||
stop_proxy()
|
||||
time.sleep(0.3)
|
||||
start_proxy()
|
||||
if _tray_icon is not None:
|
||||
_tray_icon.menu = _build_menu()
|
||||
|
||||
|
||||
def _show_error(text: str, title: str = "TG WS Proxy — Ошибка"):
|
||||
|
|
@ -271,8 +364,7 @@ def _show_info(text: str, title: str = "TG WS Proxy"):
|
|||
|
||||
|
||||
def _on_open_in_telegram(icon=None, item=None):
|
||||
host = _config.get("host", DEFAULT_CONFIG["host"])
|
||||
port = _config.get("port", DEFAULT_CONFIG["port"])
|
||||
host, port = get_local_connection_address(_config)
|
||||
url = f"tg://socks?server={host}&port={port}"
|
||||
log.info("Opening %s", url)
|
||||
try:
|
||||
|
|
@ -322,9 +414,22 @@ def _edit_config_dialog():
|
|||
FIELD_BORDER = "#d6d9dc"
|
||||
TEXT_PRIMARY = "#000000"
|
||||
TEXT_SECONDARY = "#707579"
|
||||
DISABLED_TEXT = "#9ca3af"
|
||||
DISABLED_BG = "#e5e7eb"
|
||||
FONT_FAMILY = "Segoe UI"
|
||||
|
||||
w, h = 420, 480
|
||||
def _set_widget_cursor(widget, cursor_name: str):
|
||||
try:
|
||||
widget.configure(cursor=cursor_name)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
for child in widget.winfo_children():
|
||||
_set_widget_cursor(child, cursor_name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
w, h = 420, 760
|
||||
sw = root.winfo_screenwidth()
|
||||
sh = root.winfo_screenheight()
|
||||
root.geometry(f"{w}x{h}+{(sw-w)//2}+{(sh-h)//2}")
|
||||
|
|
@ -333,32 +438,188 @@ def _edit_config_dialog():
|
|||
frame = ctk.CTkFrame(root, fg_color=BG, corner_radius=0)
|
||||
frame.pack(fill="both", expand=True, padx=24, pady=20)
|
||||
|
||||
# Host
|
||||
ctk.CTkLabel(frame, text="IP-адрес прокси",
|
||||
# Access mode (using only when advanced settings are disabled)
|
||||
ctk.CTkLabel(frame, text="Режим доступа",
|
||||
font=(FONT_FAMILY, 13), text_color=TEXT_PRIMARY,
|
||||
anchor="w").pack(anchor="w", pady=(0, 4))
|
||||
anchor="w").pack(anchor="w", pady=(0, 6))
|
||||
listen_all_var = ctk.BooleanVar(value=cfg.get("listen_all", False))
|
||||
network_mode_frame = ctk.CTkFrame(frame, fg_color="transparent")
|
||||
network_mode_frame.pack(anchor="w", pady=(0, 4))
|
||||
ctk.CTkRadioButton(
|
||||
network_mode_frame, text="Только этот компьютер (127.0.0.1)",
|
||||
variable=listen_all_var, value=False,
|
||||
font=(FONT_FAMILY, 12), text_color=TEXT_PRIMARY,
|
||||
fg_color=TG_BLUE, hover_color=TG_BLUE_HOVER,
|
||||
).pack(anchor="w")
|
||||
ctk.CTkRadioButton(
|
||||
network_mode_frame, text="Вся локальная сеть (доступ с других устройств)",
|
||||
variable=listen_all_var, value=True,
|
||||
font=(FONT_FAMILY, 12), text_color=TEXT_PRIMARY,
|
||||
fg_color=TG_BLUE, hover_color=TG_BLUE_HOVER,
|
||||
).pack(anchor="w", pady=(4, 0))
|
||||
|
||||
host_var = ctk.StringVar(value=cfg.get("host", "127.0.0.1"))
|
||||
port_var = ctk.StringVar(value=str(cfg.get("port", 1080)))
|
||||
advanced_host_var = ctk.BooleanVar(value=cfg.get("advanced_host", False))
|
||||
|
||||
def get_port_val() -> int:
|
||||
if advanced_host_var.get():
|
||||
try:
|
||||
return int(port_var.get().strip()) if port_var.get().strip() else 1080
|
||||
except ValueError:
|
||||
return 1080
|
||||
return DEFAULT_CONFIG["port"]
|
||||
|
||||
def is_network_available_here() -> bool:
|
||||
if advanced_host_var.get():
|
||||
return (host_var.get().strip() or "127.0.0.1") == "0.0.0.0"
|
||||
return listen_all_var.get()
|
||||
|
||||
def update_connection_label():
|
||||
p = get_port_val()
|
||||
local_addr = f"127.0.0.1:{p}"
|
||||
net_available = is_network_available_here()
|
||||
if net_available:
|
||||
if advanced_host_var.get():
|
||||
net_addr = f"{_get_lan_ip()}:{p}"
|
||||
else:
|
||||
net_addr = f"{_get_lan_ip()}:{p}"
|
||||
conn_label_network.configure(text=f"Локальная сеть: {net_addr}", text_color=TG_BLUE)
|
||||
copy_btn_network.configure(state="normal", fg_color=TG_BLUE)
|
||||
else:
|
||||
conn_label_network.configure(text="Локальная сеть: недоступно", text_color=TEXT_SECONDARY)
|
||||
copy_btn_network.configure(state="disabled", fg_color=DISABLED_BG)
|
||||
conn_label_local.configure(text=f"Этот компьютер: {local_addr}")
|
||||
|
||||
def copy_local_address():
|
||||
try:
|
||||
pyperclip.copy(f"127.0.0.1:{get_port_val()}")
|
||||
except Exception as exc:
|
||||
log.warning("Copy failed: %s", exc)
|
||||
|
||||
def copy_network_address():
|
||||
if not is_network_available_here():
|
||||
return
|
||||
p = get_port_val()
|
||||
net_h = _get_lan_ip()
|
||||
try:
|
||||
pyperclip.copy(f"{net_h}:{p}")
|
||||
except Exception as exc:
|
||||
log.warning("Copy failed: %s", exc)
|
||||
|
||||
def toggle_advanced():
|
||||
use_advanced = advanced_host_var.get()
|
||||
host_entry.configure(
|
||||
state="normal" if use_advanced else "disabled",
|
||||
fg_color=FIELD_BG if use_advanced else DISABLED_BG,
|
||||
text_color=TEXT_PRIMARY if use_advanced else DISABLED_TEXT,
|
||||
)
|
||||
port_entry.configure(
|
||||
state="normal" if use_advanced else "disabled",
|
||||
fg_color=FIELD_BG if use_advanced else DISABLED_BG,
|
||||
text_color=TEXT_PRIMARY if use_advanced else DISABLED_TEXT,
|
||||
)
|
||||
dc_textbox.configure(
|
||||
state="normal" if use_advanced else "disabled",
|
||||
fg_color=FIELD_BG if use_advanced else DISABLED_BG,
|
||||
text_color=TEXT_PRIMARY if use_advanced else DISABLED_TEXT,
|
||||
)
|
||||
host_label.configure(text_color=TEXT_PRIMARY if use_advanced else DISABLED_TEXT)
|
||||
port_label.configure(text_color=TEXT_PRIMARY if use_advanced else DISABLED_TEXT)
|
||||
dc_label.configure(text_color=TEXT_PRIMARY if use_advanced else DISABLED_TEXT)
|
||||
cursor = "xterm" if use_advanced else "arrow"
|
||||
_set_widget_cursor(host_entry, cursor)
|
||||
_set_widget_cursor(port_entry, cursor)
|
||||
_set_widget_cursor(dc_textbox, cursor)
|
||||
update_connection_label()
|
||||
|
||||
conn_block = ctk.CTkFrame(frame, fg_color="transparent")
|
||||
conn_block.pack(anchor="w", pady=(12, 12), fill="x")
|
||||
conn_row1 = ctk.CTkFrame(conn_block, fg_color="transparent")
|
||||
conn_row1.pack(anchor="w", fill="x", pady=(0, 2))
|
||||
conn_label_local = ctk.CTkLabel(
|
||||
conn_row1, text="", font=(FONT_FAMILY, 12, "bold"),
|
||||
text_color=TG_BLUE, anchor="w",
|
||||
)
|
||||
conn_label_local.pack(side="left")
|
||||
ctk.CTkButton(
|
||||
conn_row1, text="Копировать", width=44, height=24,
|
||||
font=(FONT_FAMILY, 11), corner_radius=6,
|
||||
fg_color=TG_BLUE, hover_color=TG_BLUE_HOVER,
|
||||
text_color="#ffffff",
|
||||
command=copy_local_address,
|
||||
).pack(side="left", padx=(8, 0))
|
||||
conn_row2 = ctk.CTkFrame(conn_block, fg_color="transparent")
|
||||
conn_row2.pack(anchor="w", fill="x", pady=(2, 0))
|
||||
conn_label_network = ctk.CTkLabel(
|
||||
conn_row2, text="", font=(FONT_FAMILY, 12, "bold"),
|
||||
text_color=TG_BLUE, anchor="w",
|
||||
)
|
||||
conn_label_network.pack(side="left")
|
||||
copy_btn_network = ctk.CTkButton(
|
||||
conn_row2, text="Копировать", width=44, height=24,
|
||||
font=(FONT_FAMILY, 11), corner_radius=6,
|
||||
fg_color=TG_BLUE, hover_color=TG_BLUE_HOVER,
|
||||
text_color="#ffffff",
|
||||
command=copy_network_address,
|
||||
)
|
||||
copy_btn_network.pack(side="left", padx=(8, 0))
|
||||
update_connection_label()
|
||||
|
||||
ctk.CTkCheckBox(
|
||||
frame, text="Расширенные настройки (IP, порт, маппинг DC)",
|
||||
variable=advanced_host_var, font=(FONT_FAMILY, 13),
|
||||
text_color=TEXT_PRIMARY,
|
||||
fg_color=TG_BLUE, hover_color=TG_BLUE_HOVER,
|
||||
corner_radius=6, border_width=2,
|
||||
border_color=FIELD_BORDER,
|
||||
).pack(anchor="w", pady=(4, 10))
|
||||
advanced_host_var.trace_add("write", lambda *a: toggle_advanced())
|
||||
|
||||
host_label = ctk.CTkLabel(
|
||||
frame, text="IP-адрес прокси",
|
||||
font=(FONT_FAMILY, 13), text_color=TEXT_PRIMARY,
|
||||
anchor="w",
|
||||
)
|
||||
host_label.pack(anchor="w", pady=(0, 4))
|
||||
host_entry = ctk.CTkEntry(frame, textvariable=host_var, width=200, height=36,
|
||||
font=(FONT_FAMILY, 13), corner_radius=10,
|
||||
fg_color=FIELD_BG, border_color=FIELD_BORDER,
|
||||
border_width=1, text_color=TEXT_PRIMARY)
|
||||
host_entry.pack(anchor="w", pady=(0, 12))
|
||||
host_var.trace_add("write", lambda *a: update_connection_label())
|
||||
listen_all_var.trace_add("write", lambda *a: update_connection_label())
|
||||
|
||||
# Port
|
||||
ctk.CTkLabel(frame, text="Порт прокси",
|
||||
port_label = ctk.CTkLabel(
|
||||
frame, text="Порт прокси",
|
||||
font=(FONT_FAMILY, 13), text_color=TEXT_PRIMARY,
|
||||
anchor="w").pack(anchor="w", pady=(0, 4))
|
||||
port_var = ctk.StringVar(value=str(cfg.get("port", 1080)))
|
||||
anchor="w",
|
||||
)
|
||||
port_label.pack(anchor="w", pady=(0, 4))
|
||||
port_entry = ctk.CTkEntry(frame, textvariable=port_var, width=120, height=36,
|
||||
font=(FONT_FAMILY, 13), corner_radius=10,
|
||||
fg_color=FIELD_BG, border_color=FIELD_BORDER,
|
||||
border_width=1, text_color=TEXT_PRIMARY)
|
||||
port_entry.pack(anchor="w", pady=(0, 12))
|
||||
port_var.trace_add("write", lambda *a: update_connection_label())
|
||||
|
||||
network_hint = ctk.CTkLabel(
|
||||
frame,
|
||||
text="В режиме «Вся локальная сеть» прокси слушает на всех интерфейсах. "
|
||||
"На других устройствах в настройках прокси Telegram укажите адрес выше. "
|
||||
"Убедитесь, что брандмауэр Windows разрешает входящие подключения на выбранный порт.",
|
||||
font=(FONT_FAMILY, 11), text_color=TEXT_SECONDARY,
|
||||
anchor="w", justify="left", wraplength=370,
|
||||
)
|
||||
network_hint.pack(anchor="w", pady=(0, 12))
|
||||
|
||||
# DC-IP mappings
|
||||
ctk.CTkLabel(frame, text="DC → IP маппинги (по одному на строку, формат DC:IP)",
|
||||
dc_label = ctk.CTkLabel(
|
||||
frame, text="DC → IP маппинги (по одному на строку, формат DC:IP)",
|
||||
font=(FONT_FAMILY, 13), text_color=TEXT_PRIMARY,
|
||||
anchor="w").pack(anchor="w", pady=(0, 4))
|
||||
anchor="w",
|
||||
)
|
||||
dc_label.pack(anchor="w", pady=(0, 4))
|
||||
dc_textbox = ctk.CTkTextbox(frame, width=370, height=120,
|
||||
font=("Consolas", 12), corner_radius=10,
|
||||
fg_color=FIELD_BG, border_color=FIELD_BORDER,
|
||||
|
|
@ -366,6 +627,27 @@ def _edit_config_dialog():
|
|||
dc_textbox.pack(anchor="w", pady=(0, 12))
|
||||
dc_textbox.insert("1.0", "\n".join(cfg.get("dc_ip", DEFAULT_CONFIG["dc_ip"])))
|
||||
|
||||
if not advanced_host_var.get():
|
||||
host_entry.configure(
|
||||
state="disabled", fg_color=DISABLED_BG, text_color=DISABLED_TEXT,
|
||||
)
|
||||
port_entry.configure(
|
||||
state="disabled", fg_color=DISABLED_BG, text_color=DISABLED_TEXT,
|
||||
)
|
||||
dc_textbox.configure(
|
||||
state="disabled", fg_color=DISABLED_BG, text_color=DISABLED_TEXT,
|
||||
)
|
||||
host_label.configure(text_color=DISABLED_TEXT)
|
||||
port_label.configure(text_color=DISABLED_TEXT)
|
||||
dc_label.configure(text_color=DISABLED_TEXT)
|
||||
_set_widget_cursor(host_entry, "arrow")
|
||||
_set_widget_cursor(port_entry, "arrow")
|
||||
_set_widget_cursor(dc_textbox, "arrow")
|
||||
else:
|
||||
_set_widget_cursor(host_entry, "xterm")
|
||||
_set_widget_cursor(port_entry, "xterm")
|
||||
_set_widget_cursor(dc_textbox, "xterm")
|
||||
|
||||
# Verbose
|
||||
verbose_var = ctk.BooleanVar(value=cfg.get("verbose", False))
|
||||
ctk.CTkCheckBox(frame, text="Подробное логирование (verbose)",
|
||||
|
|
@ -381,14 +663,15 @@ def _edit_config_dialog():
|
|||
anchor="w").pack(anchor="w", pady=(0, 16))
|
||||
|
||||
def on_save():
|
||||
import socket as _sock
|
||||
host_val = host_var.get().strip()
|
||||
advanced_host_val = advanced_host_var.get()
|
||||
listen_all_val = listen_all_var.get()
|
||||
host_val = host_var.get().strip() or "127.0.0.1"
|
||||
if advanced_host_val:
|
||||
try:
|
||||
_sock.inet_aton(host_val)
|
||||
_socket.inet_aton(host_val)
|
||||
except OSError:
|
||||
_show_error("Некорректный IP-адрес.")
|
||||
return
|
||||
|
||||
try:
|
||||
port_val = int(port_var.get().strip())
|
||||
if not (1 <= port_val <= 65535):
|
||||
|
|
@ -396,7 +679,6 @@ def _edit_config_dialog():
|
|||
except ValueError:
|
||||
_show_error("Порт должен быть числом 1-65535")
|
||||
return
|
||||
|
||||
lines = [l.strip() for l in dc_textbox.get("1.0", "end").strip().splitlines()
|
||||
if l.strip()]
|
||||
try:
|
||||
|
|
@ -404,11 +686,18 @@ def _edit_config_dialog():
|
|||
except ValueError as e:
|
||||
_show_error(str(e))
|
||||
return
|
||||
dc_ip_val = lines
|
||||
port_val_final = port_val
|
||||
else:
|
||||
port_val_final = DEFAULT_CONFIG["port"]
|
||||
dc_ip_val = DEFAULT_CONFIG["dc_ip"]
|
||||
|
||||
new_cfg = {
|
||||
"host": host_val,
|
||||
"port": port_val,
|
||||
"dc_ip": lines,
|
||||
"port": port_val_final,
|
||||
"listen_all": listen_all_val,
|
||||
"advanced_host": advanced_host_val,
|
||||
"dc_ip": dc_ip_val,
|
||||
"verbose": verbose_var.get(),
|
||||
}
|
||||
save_config(new_cfg)
|
||||
|
|
@ -478,9 +767,10 @@ def _show_first_run():
|
|||
if FIRST_RUN_MARKER.exists():
|
||||
return
|
||||
|
||||
host = _config.get("host", DEFAULT_CONFIG["host"])
|
||||
port = _config.get("port", DEFAULT_CONFIG["port"])
|
||||
tg_url = f"tg://socks?server={host}&port={port}"
|
||||
local_host, port = get_local_connection_address(_config)
|
||||
net_available = is_network_available(_config)
|
||||
net_host, _ = get_network_connection_address(_config)
|
||||
tg_url = f"tg://socks?server={local_host}&port={port}"
|
||||
|
||||
if ctk is None:
|
||||
FIRST_RUN_MARKER.touch()
|
||||
|
|
@ -503,7 +793,7 @@ def _show_first_run():
|
|||
root.resizable(False, False)
|
||||
root.attributes("-topmost", True)
|
||||
|
||||
w, h = 520, 440
|
||||
w, h = 520, 520
|
||||
sw = root.winfo_screenwidth()
|
||||
sh = root.winfo_screenheight()
|
||||
root.geometry(f"{w}x{h}+{(sw-w)//2}+{(sh-h)//2}")
|
||||
|
|
@ -524,6 +814,24 @@ def _show_first_run():
|
|||
font=(FONT_FAMILY, 17, "bold"),
|
||||
text_color=TEXT_PRIMARY).pack(side="left")
|
||||
|
||||
ctk.CTkLabel(frame, text=f"На этом устройстве: {local_host}:{port}",
|
||||
font=(FONT_FAMILY, 15, "bold"), text_color=TG_BLUE,
|
||||
anchor="w").pack(anchor="w", pady=(0, 4))
|
||||
if net_available:
|
||||
net_text = f"В локальной сети (другие устройства): {net_host}:{port}"
|
||||
net_color = TG_BLUE
|
||||
else:
|
||||
net_text = "В локальной сети: недоступно"
|
||||
net_color = TEXT_SECONDARY
|
||||
ctk.CTkLabel(frame, text=net_text,
|
||||
font=(FONT_FAMILY, 14, "bold"), text_color=net_color,
|
||||
anchor="w").pack(anchor="w", pady=(0, 12))
|
||||
|
||||
if net_available:
|
||||
ctk.CTkLabel(frame, text="С других устройств в настройках прокси Telegram укажите адрес «В локальной сети».",
|
||||
font=(FONT_FAMILY, 12), text_color=TEXT_SECONDARY,
|
||||
anchor="w", justify="left", wraplength=460).pack(anchor="w", pady=(0, 12))
|
||||
|
||||
# Info sections
|
||||
sections = [
|
||||
("Как подключить Telegram Desktop:", True),
|
||||
|
|
@ -532,7 +840,8 @@ def _show_first_run():
|
|||
(f" Или ссылка: {tg_url}", False),
|
||||
("\n Вручную:", True),
|
||||
(" Настройки → Продвинутые → Тип подключения → Прокси", False),
|
||||
(f" SOCKS5 → {host} : {port} (без логина/пароля)", False),
|
||||
(f" На этом устройстве: SOCKS5 → {local_host} : {port} (без логина/пароля)", False),
|
||||
(f" В сети: SOCKS5 → {net_host} : {port}" if net_available else " В сети: недоступно", False),
|
||||
]
|
||||
|
||||
for text, bold in sections:
|
||||
|
|
@ -575,16 +884,46 @@ def _show_first_run():
|
|||
root.mainloop()
|
||||
|
||||
|
||||
def _on_copy_local_address():
|
||||
host, port = get_local_connection_address(_config)
|
||||
try:
|
||||
pyperclip.copy(f"{host}:{port}")
|
||||
except Exception as exc:
|
||||
log.warning("Copy failed: %s", exc)
|
||||
|
||||
|
||||
def _on_copy_network_address():
|
||||
if not is_network_available(_config):
|
||||
return
|
||||
host, port = get_network_connection_address(_config)
|
||||
try:
|
||||
pyperclip.copy(f"{host}:{port}")
|
||||
except Exception as exc:
|
||||
log.warning("Copy failed: %s", exc)
|
||||
|
||||
|
||||
def _build_menu():
|
||||
if pystray is None:
|
||||
return None
|
||||
host = _config.get("host", DEFAULT_CONFIG["host"])
|
||||
port = _config.get("port", DEFAULT_CONFIG["port"])
|
||||
local_host, port = get_local_connection_address(_config)
|
||||
net_available = is_network_available(_config)
|
||||
net_host, _ = get_network_connection_address(_config)
|
||||
if net_available:
|
||||
net_menu_text = f"Копировать адрес (локальная сеть) — {net_host}:{port}"
|
||||
net_menu_callback = _on_copy_network_address
|
||||
else:
|
||||
net_menu_text = "Копировать адрес (локальная сеть) — недоступно"
|
||||
net_menu_callback = lambda *a: None # не копируем при недоступности
|
||||
return pystray.Menu(
|
||||
pystray.MenuItem(
|
||||
f"Открыть в Telegram ({host}:{port})",
|
||||
f"Открыть в Telegram ({local_host}:{port})",
|
||||
_on_open_in_telegram,
|
||||
default=True),
|
||||
pystray.MenuItem(
|
||||
f"Копировать адрес (этот компьютер) — {local_host}:{port}",
|
||||
_on_copy_local_address,
|
||||
),
|
||||
pystray.MenuItem(net_menu_text, net_menu_callback),
|
||||
pystray.Menu.SEPARATOR,
|
||||
pystray.MenuItem("Перезапустить прокси", _on_restart),
|
||||
pystray.MenuItem("Настройки...", _on_edit_config),
|
||||
|
|
|
|||
Loading…
Reference in New Issue