fix: xdg-open not working
This commit is contained in:
parent
d660f6a270
commit
9d732a3cce
364
linux.py
364
linux.py
|
|
@ -1,29 +1,27 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio as _asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import psutil
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import webbrowser
|
import webbrowser
|
||||||
import pyperclip
|
|
||||||
import asyncio as _asyncio
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
import pystray
|
|
||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
|
import psutil
|
||||||
|
import pyperclip
|
||||||
|
import pystray
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
import proxy.tg_ws_proxy as tg_ws_proxy
|
import proxy.tg_ws_proxy as tg_ws_proxy
|
||||||
|
|
||||||
|
|
||||||
APP_NAME = "TgWsProxy"
|
APP_NAME = "TgWsProxy"
|
||||||
APP_DIR = Path(os.environ.get("XDG_CONFIG_HOME",
|
APP_DIR = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / APP_NAME
|
||||||
Path.home() / ".config")) / APP_NAME
|
|
||||||
CONFIG_FILE = APP_DIR / "config.json"
|
CONFIG_FILE = APP_DIR / "config.json"
|
||||||
LOG_FILE = APP_DIR / "proxy.log"
|
LOG_FILE = APP_DIR / "proxy.log"
|
||||||
FIRST_RUN_MARKER = APP_DIR / ".first_run_done"
|
FIRST_RUN_MARKER = APP_DIR / ".first_run_done"
|
||||||
|
|
@ -120,8 +118,7 @@ def _acquire_lock() -> bool:
|
||||||
payload = {
|
payload = {
|
||||||
"create_time": proc.create_time(),
|
"create_time": proc.create_time(),
|
||||||
}
|
}
|
||||||
lock_file.write_text(json.dumps(payload, ensure_ascii=False),
|
lock_file.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
|
||||||
encoding="utf-8")
|
|
||||||
except Exception:
|
except Exception:
|
||||||
lock_file.touch()
|
lock_file.touch()
|
||||||
|
|
||||||
|
|
@ -160,17 +157,22 @@ def setup_logging(verbose: bool = False):
|
||||||
|
|
||||||
fh = logging.FileHandler(str(LOG_FILE), encoding="utf-8")
|
fh = logging.FileHandler(str(LOG_FILE), encoding="utf-8")
|
||||||
fh.setLevel(logging.DEBUG)
|
fh.setLevel(logging.DEBUG)
|
||||||
fh.setFormatter(logging.Formatter(
|
fh.setFormatter(
|
||||||
"%(asctime)s %(levelname)-5s %(name)s %(message)s",
|
logging.Formatter(
|
||||||
datefmt="%Y-%m-%d %H:%M:%S"))
|
"%(asctime)s %(levelname)-5s %(name)s %(message)s",
|
||||||
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
|
)
|
||||||
|
)
|
||||||
root.addHandler(fh)
|
root.addHandler(fh)
|
||||||
|
|
||||||
if not getattr(sys, "frozen", False):
|
if not getattr(sys, "frozen", False):
|
||||||
ch = logging.StreamHandler(sys.stdout)
|
ch = logging.StreamHandler(sys.stdout)
|
||||||
ch.setLevel(logging.DEBUG if verbose else logging.INFO)
|
ch.setLevel(logging.DEBUG if verbose else logging.INFO)
|
||||||
ch.setFormatter(logging.Formatter(
|
ch.setFormatter(
|
||||||
"%(asctime)s %(levelname)-5s %(message)s",
|
logging.Formatter(
|
||||||
datefmt="%H:%M:%S"))
|
"%(asctime)s %(levelname)-5s %(message)s", datefmt="%H:%M:%S"
|
||||||
|
)
|
||||||
|
)
|
||||||
root.addHandler(ch)
|
root.addHandler(ch)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -181,18 +183,20 @@ def _make_icon_image(size: int = 64):
|
||||||
draw = ImageDraw.Draw(img)
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
margin = 2
|
margin = 2
|
||||||
draw.ellipse([margin, margin, size - margin, size - margin],
|
draw.ellipse(
|
||||||
fill=(0, 136, 204, 255))
|
[margin, margin, size - margin, size - margin], fill=(0, 136, 204, 255)
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
font = ImageFont.truetype(
|
font = ImageFont.truetype(
|
||||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
||||||
size=int(size * 0.55))
|
size=int(size * 0.55),
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
font = ImageFont.truetype(
|
font = ImageFont.truetype(
|
||||||
"/usr/share/fonts/TTF/DejaVuSans-Bold.ttf",
|
"/usr/share/fonts/TTF/DejaVuSans-Bold.ttf", size=int(size * 0.55)
|
||||||
size=int(size * 0.55))
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
font = ImageFont.load_default()
|
font = ImageFont.load_default()
|
||||||
bbox = draw.textbbox((0, 0), "T", font=font)
|
bbox = draw.textbbox((0, 0), "T", font=font)
|
||||||
|
|
@ -214,9 +218,9 @@ def _load_icon():
|
||||||
return _make_icon_image()
|
return _make_icon_image()
|
||||||
|
|
||||||
|
|
||||||
|
def _run_proxy_thread(
|
||||||
def _run_proxy_thread(port: int, dc_opt: Dict[int, str], verbose: bool,
|
port: int, dc_opt: Dict[int, str], verbose: bool, host: str = "127.0.0.1"
|
||||||
host: str = '127.0.0.1'):
|
):
|
||||||
global _async_stop
|
global _async_stop
|
||||||
loop = _asyncio.new_event_loop()
|
loop = _asyncio.new_event_loop()
|
||||||
_asyncio.set_event_loop(loop)
|
_asyncio.set_event_loop(loop)
|
||||||
|
|
@ -225,11 +229,14 @@ def _run_proxy_thread(port: int, dc_opt: Dict[int, str], verbose: bool,
|
||||||
|
|
||||||
try:
|
try:
|
||||||
loop.run_until_complete(
|
loop.run_until_complete(
|
||||||
tg_ws_proxy._run(port, dc_opt, stop_event=stop_ev, host=host))
|
tg_ws_proxy._run(port, dc_opt, stop_event=stop_ev, host=host)
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log.error("Proxy thread crashed: %s", exc)
|
log.error("Proxy thread crashed: %s", exc)
|
||||||
if "Address already in use" in str(exc):
|
if "Address already in use" in str(exc):
|
||||||
_show_error("Не удалось запустить прокси:\nПорт уже используется другим приложением.\n\nЗакройте приложение, использующее этот порт, или измените порт в настройках прокси и перезапустите.")
|
_show_error(
|
||||||
|
"Не удалось запустить прокси:\nПорт уже используется другим приложением.\n\nЗакройте приложение, использующее этот порт, или измените порт в настройках прокси и перезапустите."
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
loop.close()
|
loop.close()
|
||||||
_async_stop = None
|
_async_stop = None
|
||||||
|
|
@ -258,7 +265,9 @@ def start_proxy():
|
||||||
_proxy_thread = threading.Thread(
|
_proxy_thread = threading.Thread(
|
||||||
target=_run_proxy_thread,
|
target=_run_proxy_thread,
|
||||||
args=(port, dc_opt, verbose, host),
|
args=(port, dc_opt, verbose, host),
|
||||||
daemon=True, name="proxy")
|
daemon=True,
|
||||||
|
name="proxy",
|
||||||
|
)
|
||||||
_proxy_thread.start()
|
_proxy_thread.start()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -283,6 +292,7 @@ def restart_proxy():
|
||||||
def _show_error(text: str, title: str = "TG WS Proxy — Ошибка"):
|
def _show_error(text: str, title: str = "TG WS Proxy — Ошибка"):
|
||||||
import tkinter as _tk
|
import tkinter as _tk
|
||||||
from tkinter import messagebox as _mb
|
from tkinter import messagebox as _mb
|
||||||
|
|
||||||
root = _tk.Tk()
|
root = _tk.Tk()
|
||||||
root.withdraw()
|
root.withdraw()
|
||||||
_mb.showerror(title, text, parent=root)
|
_mb.showerror(title, text, parent=root)
|
||||||
|
|
@ -292,6 +302,7 @@ def _show_error(text: str, title: str = "TG WS Proxy — Ошибка"):
|
||||||
def _show_info(text: str, title: str = "TG WS Proxy"):
|
def _show_info(text: str, title: str = "TG WS Proxy"):
|
||||||
import tkinter as _tk
|
import tkinter as _tk
|
||||||
from tkinter import messagebox as _mb
|
from tkinter import messagebox as _mb
|
||||||
|
|
||||||
root = _tk.Tk()
|
root = _tk.Tk()
|
||||||
root.withdraw()
|
root.withdraw()
|
||||||
_mb.showinfo(title, text, parent=root)
|
_mb.showinfo(title, text, parent=root)
|
||||||
|
|
@ -301,28 +312,17 @@ def _show_info(text: str, title: str = "TG WS Proxy"):
|
||||||
def _on_open_in_telegram(icon=None, item=None):
|
def _on_open_in_telegram(icon=None, item=None):
|
||||||
port = _config.get("port", DEFAULT_CONFIG["port"])
|
port = _config.get("port", DEFAULT_CONFIG["port"])
|
||||||
url = f"tg://socks?server=127.0.0.1&port={port}"
|
url = f"tg://socks?server=127.0.0.1&port={port}"
|
||||||
log.info("Opening %s", url)
|
log.info("Copying %s", url)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.call(['xdg-open', url])
|
pyperclip.copy(url)
|
||||||
if result != 0:
|
_show_info(
|
||||||
raise RuntimeError("xdg-open failed")
|
f"Ссылка скопирована в буфер обмена, отправьте её в Telegram и нажмите по ней ЛКМ:\n{url}",
|
||||||
except Exception:
|
"TG WS Proxy",
|
||||||
log.info("xdg-open failed, trying webbrowser")
|
)
|
||||||
try:
|
except Exception as exc:
|
||||||
result = webbrowser.open(url)
|
log.error("Clipboard copy failed: %s", exc)
|
||||||
if not result:
|
_show_error(f"Не удалось скопировать ссылку:\n{exc}")
|
||||||
raise RuntimeError("webbrowser.open returned False")
|
|
||||||
except Exception:
|
|
||||||
log.info("Browser open failed, copying to clipboard")
|
|
||||||
try:
|
|
||||||
pyperclip.copy(url)
|
|
||||||
_show_info(
|
|
||||||
f"Не удалось открыть Telegram автоматически.\n\n"
|
|
||||||
f"Ссылка скопирована в буфер обмена, отправьте её в Telegram и нажмите по ней ЛКМ:\n{url}",
|
|
||||||
"TG WS Proxy")
|
|
||||||
except Exception as exc:
|
|
||||||
log.error("Clipboard copy failed: %s", exc)
|
|
||||||
_show_error(f"Не удалось скопировать ссылку:\n{exc}")
|
|
||||||
|
|
||||||
|
|
||||||
def _on_restart(icon=None, item=None):
|
def _on_restart(icon=None, item=None):
|
||||||
|
|
@ -351,6 +351,7 @@ def _edit_config_dialog():
|
||||||
icon_img = _load_icon()
|
icon_img = _load_icon()
|
||||||
if icon_img:
|
if icon_img:
|
||||||
from PIL import ImageTk
|
from PIL import ImageTk
|
||||||
|
|
||||||
_photo = ImageTk.PhotoImage(icon_img.resize((64, 64)))
|
_photo = ImageTk.PhotoImage(icon_img.resize((64, 64)))
|
||||||
root.iconphoto(False, _photo)
|
root.iconphoto(False, _photo)
|
||||||
|
|
||||||
|
|
@ -366,61 +367,107 @@ def _edit_config_dialog():
|
||||||
w, h = 420, 480
|
w, h = 420, 480
|
||||||
sw = root.winfo_screenwidth()
|
sw = root.winfo_screenwidth()
|
||||||
sh = root.winfo_screenheight()
|
sh = root.winfo_screenheight()
|
||||||
root.geometry(f"{w}x{h}+{(sw-w)//2}+{(sh-h)//2}")
|
root.geometry(f"{w}x{h}+{(sw - w) // 2}+{(sh - h) // 2}")
|
||||||
root.configure(fg_color=BG)
|
root.configure(fg_color=BG)
|
||||||
|
|
||||||
frame = ctk.CTkFrame(root, fg_color=BG, corner_radius=0)
|
frame = ctk.CTkFrame(root, fg_color=BG, corner_radius=0)
|
||||||
frame.pack(fill="both", expand=True, padx=24, pady=20)
|
frame.pack(fill="both", expand=True, padx=24, pady=20)
|
||||||
|
|
||||||
# Host
|
# Host
|
||||||
ctk.CTkLabel(frame, text="IP-адрес прокси",
|
ctk.CTkLabel(
|
||||||
font=(FONT_FAMILY, 13), text_color=TEXT_PRIMARY,
|
frame,
|
||||||
anchor="w").pack(anchor="w", pady=(0, 4))
|
text="IP-адрес прокси",
|
||||||
|
font=(FONT_FAMILY, 13),
|
||||||
|
text_color=TEXT_PRIMARY,
|
||||||
|
anchor="w",
|
||||||
|
).pack(anchor="w", pady=(0, 4))
|
||||||
host_var = ctk.StringVar(value=cfg.get("host", "127.0.0.1"))
|
host_var = ctk.StringVar(value=cfg.get("host", "127.0.0.1"))
|
||||||
host_entry = ctk.CTkEntry(frame, textvariable=host_var, width=200, height=36,
|
host_entry = ctk.CTkEntry(
|
||||||
font=(FONT_FAMILY, 13), corner_radius=10,
|
frame,
|
||||||
fg_color=FIELD_BG, border_color=FIELD_BORDER,
|
textvariable=host_var,
|
||||||
border_width=1, text_color=TEXT_PRIMARY)
|
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_entry.pack(anchor="w", pady=(0, 12))
|
||||||
|
|
||||||
# Port
|
# Port
|
||||||
ctk.CTkLabel(frame, text="Порт прокси",
|
ctk.CTkLabel(
|
||||||
font=(FONT_FAMILY, 13), text_color=TEXT_PRIMARY,
|
frame,
|
||||||
anchor="w").pack(anchor="w", pady=(0, 4))
|
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)))
|
port_var = ctk.StringVar(value=str(cfg.get("port", 1080)))
|
||||||
port_entry = ctk.CTkEntry(frame, textvariable=port_var, width=120, height=36,
|
port_entry = ctk.CTkEntry(
|
||||||
font=(FONT_FAMILY, 13), corner_radius=10,
|
frame,
|
||||||
fg_color=FIELD_BG, border_color=FIELD_BORDER,
|
textvariable=port_var,
|
||||||
border_width=1, text_color=TEXT_PRIMARY)
|
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_entry.pack(anchor="w", pady=(0, 12))
|
||||||
|
|
||||||
# DC-IP mappings
|
# DC-IP mappings
|
||||||
ctk.CTkLabel(frame, text="DC → IP маппинги (по одному на строку, формат DC:IP)",
|
ctk.CTkLabel(
|
||||||
font=(FONT_FAMILY, 13), text_color=TEXT_PRIMARY,
|
frame,
|
||||||
anchor="w").pack(anchor="w", pady=(0, 4))
|
text="DC → IP маппинги (по одному на строку, формат DC:IP)",
|
||||||
dc_textbox = ctk.CTkTextbox(frame, width=370, height=120,
|
font=(FONT_FAMILY, 13),
|
||||||
font=("Monospace", 12), corner_radius=10,
|
text_color=TEXT_PRIMARY,
|
||||||
fg_color=FIELD_BG, border_color=FIELD_BORDER,
|
anchor="w",
|
||||||
border_width=1, text_color=TEXT_PRIMARY)
|
).pack(anchor="w", pady=(0, 4))
|
||||||
|
dc_textbox = ctk.CTkTextbox(
|
||||||
|
frame,
|
||||||
|
width=370,
|
||||||
|
height=120,
|
||||||
|
font=("Monospace", 12),
|
||||||
|
corner_radius=10,
|
||||||
|
fg_color=FIELD_BG,
|
||||||
|
border_color=FIELD_BORDER,
|
||||||
|
border_width=1,
|
||||||
|
text_color=TEXT_PRIMARY,
|
||||||
|
)
|
||||||
dc_textbox.pack(anchor="w", pady=(0, 12))
|
dc_textbox.pack(anchor="w", pady=(0, 12))
|
||||||
dc_textbox.insert("1.0", "\n".join(cfg.get("dc_ip", DEFAULT_CONFIG["dc_ip"])))
|
dc_textbox.insert("1.0", "\n".join(cfg.get("dc_ip", DEFAULT_CONFIG["dc_ip"])))
|
||||||
|
|
||||||
# Verbose
|
# Verbose
|
||||||
verbose_var = ctk.BooleanVar(value=cfg.get("verbose", False))
|
verbose_var = ctk.BooleanVar(value=cfg.get("verbose", False))
|
||||||
ctk.CTkCheckBox(frame, text="Подробное логирование (verbose)",
|
ctk.CTkCheckBox(
|
||||||
variable=verbose_var, font=(FONT_FAMILY, 13),
|
frame,
|
||||||
text_color=TEXT_PRIMARY,
|
text="Подробное логирование (verbose)",
|
||||||
fg_color=TG_BLUE, hover_color=TG_BLUE_HOVER,
|
variable=verbose_var,
|
||||||
corner_radius=6, border_width=2,
|
font=(FONT_FAMILY, 13),
|
||||||
border_color=FIELD_BORDER).pack(anchor="w", pady=(0, 8))
|
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=(0, 8))
|
||||||
|
|
||||||
# Info label
|
# Info label
|
||||||
ctk.CTkLabel(frame, text="Изменения вступят в силу после перезапуска прокси.",
|
ctk.CTkLabel(
|
||||||
font=(FONT_FAMILY, 11), text_color=TEXT_SECONDARY,
|
frame,
|
||||||
anchor="w").pack(anchor="w", pady=(0, 16))
|
text="Изменения вступят в силу после перезапуска прокси.",
|
||||||
|
font=(FONT_FAMILY, 11),
|
||||||
|
text_color=TEXT_SECONDARY,
|
||||||
|
anchor="w",
|
||||||
|
).pack(anchor="w", pady=(0, 16))
|
||||||
|
|
||||||
def on_save():
|
def on_save():
|
||||||
import socket as _sock
|
import socket as _sock
|
||||||
|
|
||||||
host_val = host_var.get().strip()
|
host_val = host_var.get().strip()
|
||||||
try:
|
try:
|
||||||
_sock.inet_aton(host_val)
|
_sock.inet_aton(host_val)
|
||||||
|
|
@ -436,8 +483,11 @@ def _edit_config_dialog():
|
||||||
_show_error("Порт должен быть числом 1-65535")
|
_show_error("Порт должен быть числом 1-65535")
|
||||||
return
|
return
|
||||||
|
|
||||||
lines = [l.strip() for l in dc_textbox.get("1.0", "end").strip().splitlines()
|
lines = [
|
||||||
if l.strip()]
|
l.strip()
|
||||||
|
for l in dc_textbox.get("1.0", "end").strip().splitlines()
|
||||||
|
if l.strip()
|
||||||
|
]
|
||||||
try:
|
try:
|
||||||
tg_ws_proxy.parse_dc_ip_list(lines)
|
tg_ws_proxy.parse_dc_ip_list(lines)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
|
@ -457,10 +507,12 @@ def _edit_config_dialog():
|
||||||
_tray_icon.menu = _build_menu()
|
_tray_icon.menu = _build_menu()
|
||||||
|
|
||||||
from tkinter import messagebox
|
from tkinter import messagebox
|
||||||
if messagebox.askyesno("Перезапустить?",
|
|
||||||
"Настройки сохранены.\n\n"
|
if messagebox.askyesno(
|
||||||
"Перезапустить прокси сейчас?",
|
"Перезапустить?",
|
||||||
parent=root):
|
"Настройки сохранены.\n\nПерезапустить прокси сейчас?",
|
||||||
|
parent=root,
|
||||||
|
):
|
||||||
root.destroy()
|
root.destroy()
|
||||||
restart_proxy()
|
restart_proxy()
|
||||||
else:
|
else:
|
||||||
|
|
@ -471,17 +523,32 @@ def _edit_config_dialog():
|
||||||
|
|
||||||
btn_frame = ctk.CTkFrame(frame, fg_color="transparent")
|
btn_frame = ctk.CTkFrame(frame, fg_color="transparent")
|
||||||
btn_frame.pack(fill="x")
|
btn_frame.pack(fill="x")
|
||||||
ctk.CTkButton(btn_frame, text="Сохранить", width=140, height=38,
|
ctk.CTkButton(
|
||||||
font=(FONT_FAMILY, 14, "bold"), corner_radius=10,
|
btn_frame,
|
||||||
fg_color=TG_BLUE, hover_color=TG_BLUE_HOVER,
|
text="Сохранить",
|
||||||
text_color="#ffffff",
|
width=140,
|
||||||
command=on_save).pack(side="left", padx=(0, 10))
|
height=38,
|
||||||
ctk.CTkButton(btn_frame, text="Отмена", width=140, height=38,
|
font=(FONT_FAMILY, 14, "bold"),
|
||||||
font=(FONT_FAMILY, 14), corner_radius=10,
|
corner_radius=10,
|
||||||
fg_color=FIELD_BG, hover_color=FIELD_BORDER,
|
fg_color=TG_BLUE,
|
||||||
text_color=TEXT_PRIMARY, border_width=1,
|
hover_color=TG_BLUE_HOVER,
|
||||||
border_color=FIELD_BORDER,
|
text_color="#ffffff",
|
||||||
command=on_cancel).pack(side="left")
|
command=on_save,
|
||||||
|
).pack(side="left", padx=(0, 10))
|
||||||
|
ctk.CTkButton(
|
||||||
|
btn_frame,
|
||||||
|
text="Отмена",
|
||||||
|
width=140,
|
||||||
|
height=38,
|
||||||
|
font=(FONT_FAMILY, 14),
|
||||||
|
corner_radius=10,
|
||||||
|
fg_color=FIELD_BG,
|
||||||
|
hover_color=FIELD_BORDER,
|
||||||
|
text_color=TEXT_PRIMARY,
|
||||||
|
border_width=1,
|
||||||
|
border_color=FIELD_BORDER,
|
||||||
|
command=on_cancel,
|
||||||
|
).pack(side="left")
|
||||||
|
|
||||||
root.mainloop()
|
root.mainloop()
|
||||||
|
|
||||||
|
|
@ -489,7 +556,19 @@ def _edit_config_dialog():
|
||||||
def _on_open_logs(icon=None, item=None):
|
def _on_open_logs(icon=None, item=None):
|
||||||
log.info("Opening log file: %s", LOG_FILE)
|
log.info("Opening log file: %s", LOG_FILE)
|
||||||
if LOG_FILE.exists():
|
if LOG_FILE.exists():
|
||||||
subprocess.Popen(['xdg-open', str(LOG_FILE)])
|
env = os.environ.copy()
|
||||||
|
env.pop("VIRTUAL_ENV", None)
|
||||||
|
env.pop("PYTHONPATH", None)
|
||||||
|
env.pop("PYTHONHOME", None)
|
||||||
|
|
||||||
|
subprocess.Popen(
|
||||||
|
["xdg-open", str(LOG_FILE)],
|
||||||
|
env=env,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
start_new_session=True,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
_show_info("Файл логов ещё не создан.", "TG WS Proxy")
|
_show_info("Файл логов ещё не создан.", "TG WS Proxy")
|
||||||
|
|
||||||
|
|
@ -505,13 +584,13 @@ def _on_exit(icon=None, item=None):
|
||||||
def _force_exit():
|
def _force_exit():
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
|
|
||||||
threading.Thread(target=_force_exit, daemon=True, name="force-exit").start()
|
threading.Thread(target=_force_exit, daemon=True, name="force-exit").start()
|
||||||
|
|
||||||
if icon:
|
if icon:
|
||||||
icon.stop()
|
icon.stop()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _show_first_run():
|
def _show_first_run():
|
||||||
_ensure_dirs()
|
_ensure_dirs()
|
||||||
if FIRST_RUN_MARKER.exists():
|
if FIRST_RUN_MARKER.exists():
|
||||||
|
|
@ -545,13 +624,14 @@ def _show_first_run():
|
||||||
icon_img = _load_icon()
|
icon_img = _load_icon()
|
||||||
if icon_img:
|
if icon_img:
|
||||||
from PIL import ImageTk
|
from PIL import ImageTk
|
||||||
|
|
||||||
_photo = ImageTk.PhotoImage(icon_img.resize((64, 64)))
|
_photo = ImageTk.PhotoImage(icon_img.resize((64, 64)))
|
||||||
root.iconphoto(False, _photo)
|
root.iconphoto(False, _photo)
|
||||||
|
|
||||||
w, h = 520, 440
|
w, h = 520, 440
|
||||||
sw = root.winfo_screenwidth()
|
sw = root.winfo_screenwidth()
|
||||||
sh = root.winfo_screenheight()
|
sh = root.winfo_screenheight()
|
||||||
root.geometry(f"{w}x{h}+{(sw-w)//2}+{(sh-h)//2}")
|
root.geometry(f"{w}x{h}+{(sw - w) // 2}+{(sh - h) // 2}")
|
||||||
root.configure(fg_color=BG)
|
root.configure(fg_color=BG)
|
||||||
|
|
||||||
frame = ctk.CTkFrame(root, fg_color=BG, corner_radius=0)
|
frame = ctk.CTkFrame(root, fg_color=BG, corner_radius=0)
|
||||||
|
|
@ -561,13 +641,17 @@ def _show_first_run():
|
||||||
title_frame.pack(anchor="w", pady=(0, 16), fill="x")
|
title_frame.pack(anchor="w", pady=(0, 16), fill="x")
|
||||||
|
|
||||||
# Blue accent bar
|
# Blue accent bar
|
||||||
accent_bar = ctk.CTkFrame(title_frame, fg_color=TG_BLUE,
|
accent_bar = ctk.CTkFrame(
|
||||||
width=4, height=32, corner_radius=2)
|
title_frame, fg_color=TG_BLUE, width=4, height=32, corner_radius=2
|
||||||
|
)
|
||||||
accent_bar.pack(side="left", padx=(0, 12))
|
accent_bar.pack(side="left", padx=(0, 12))
|
||||||
|
|
||||||
ctk.CTkLabel(title_frame, text="Прокси запущен и работает в системном трее",
|
ctk.CTkLabel(
|
||||||
font=(FONT_FAMILY, 17, "bold"),
|
title_frame,
|
||||||
text_color=TEXT_PRIMARY).pack(side="left")
|
text="Прокси запущен и работает в системном трее",
|
||||||
|
font=(FONT_FAMILY, 17, "bold"),
|
||||||
|
text_color=TEXT_PRIMARY,
|
||||||
|
).pack(side="left")
|
||||||
|
|
||||||
# Info sections
|
# Info sections
|
||||||
sections = [
|
sections = [
|
||||||
|
|
@ -582,26 +666,37 @@ def _show_first_run():
|
||||||
|
|
||||||
for text, bold in sections:
|
for text, bold in sections:
|
||||||
weight = "bold" if bold else "normal"
|
weight = "bold" if bold else "normal"
|
||||||
ctk.CTkLabel(frame, text=text,
|
ctk.CTkLabel(
|
||||||
font=(FONT_FAMILY, 13, weight),
|
frame,
|
||||||
text_color=TEXT_PRIMARY,
|
text=text,
|
||||||
anchor="w", justify="left").pack(anchor="w", pady=1)
|
font=(FONT_FAMILY, 13, weight),
|
||||||
|
text_color=TEXT_PRIMARY,
|
||||||
|
anchor="w",
|
||||||
|
justify="left",
|
||||||
|
).pack(anchor="w", pady=1)
|
||||||
|
|
||||||
# Spacer
|
# Spacer
|
||||||
ctk.CTkFrame(frame, fg_color="transparent", height=16).pack()
|
ctk.CTkFrame(frame, fg_color="transparent", height=16).pack()
|
||||||
|
|
||||||
# Separator
|
# Separator
|
||||||
ctk.CTkFrame(frame, fg_color=FIELD_BORDER, height=1,
|
ctk.CTkFrame(frame, fg_color=FIELD_BORDER, height=1, corner_radius=0).pack(
|
||||||
corner_radius=0).pack(fill="x", pady=(0, 12))
|
fill="x", pady=(0, 12)
|
||||||
|
)
|
||||||
|
|
||||||
# Checkbox
|
# Checkbox
|
||||||
auto_var = ctk.BooleanVar(value=True)
|
auto_var = ctk.BooleanVar(value=True)
|
||||||
ctk.CTkCheckBox(frame, text="Открыть прокси в Telegram сейчас",
|
ctk.CTkCheckBox(
|
||||||
variable=auto_var, font=(FONT_FAMILY, 13),
|
frame,
|
||||||
text_color=TEXT_PRIMARY,
|
text="Открыть прокси в Telegram сейчас",
|
||||||
fg_color=TG_BLUE, hover_color=TG_BLUE_HOVER,
|
variable=auto_var,
|
||||||
corner_radius=6, border_width=2,
|
font=(FONT_FAMILY, 13),
|
||||||
border_color=FIELD_BORDER).pack(anchor="w", pady=(0, 16))
|
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=(0, 16))
|
||||||
|
|
||||||
def on_ok():
|
def on_ok():
|
||||||
FIRST_RUN_MARKER.touch()
|
FIRST_RUN_MARKER.touch()
|
||||||
|
|
@ -610,11 +705,18 @@ def _show_first_run():
|
||||||
if open_tg:
|
if open_tg:
|
||||||
_on_open_in_telegram()
|
_on_open_in_telegram()
|
||||||
|
|
||||||
ctk.CTkButton(frame, text="Начать", width=180, height=42,
|
ctk.CTkButton(
|
||||||
font=(FONT_FAMILY, 15, "bold"), corner_radius=10,
|
frame,
|
||||||
fg_color=TG_BLUE, hover_color=TG_BLUE_HOVER,
|
text="Начать",
|
||||||
text_color="#ffffff",
|
width=180,
|
||||||
command=on_ok).pack(pady=(0, 0))
|
height=42,
|
||||||
|
font=(FONT_FAMILY, 15, "bold"),
|
||||||
|
corner_radius=10,
|
||||||
|
fg_color=TG_BLUE,
|
||||||
|
hover_color=TG_BLUE_HOVER,
|
||||||
|
text_color="#ffffff",
|
||||||
|
command=on_ok,
|
||||||
|
).pack(pady=(0, 0))
|
||||||
|
|
||||||
root.protocol("WM_DELETE_WINDOW", on_ok)
|
root.protocol("WM_DELETE_WINDOW", on_ok)
|
||||||
root.mainloop()
|
root.mainloop()
|
||||||
|
|
@ -622,17 +724,18 @@ def _show_first_run():
|
||||||
|
|
||||||
def _has_ipv6_enabled() -> bool:
|
def _has_ipv6_enabled() -> bool:
|
||||||
import socket as _sock
|
import socket as _sock
|
||||||
|
|
||||||
try:
|
try:
|
||||||
addrs = _sock.getaddrinfo(_sock.gethostname(), None, _sock.AF_INET6)
|
addrs = _sock.getaddrinfo(_sock.gethostname(), None, _sock.AF_INET6)
|
||||||
for addr in addrs:
|
for addr in addrs:
|
||||||
ip = addr[4][0]
|
ip = addr[4][0]
|
||||||
if ip and not ip.startswith('::1') and not ip.startswith('fe80::1'):
|
if ip and not ip.startswith("::1") and not ip.startswith("fe80::1"):
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
s = _sock.socket(_sock.AF_INET6, _sock.SOCK_STREAM)
|
s = _sock.socket(_sock.AF_INET6, _sock.SOCK_STREAM)
|
||||||
s.bind(('::1', 0))
|
s.bind(("::1", 0))
|
||||||
s.close()
|
s.close()
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -662,7 +765,8 @@ def _show_ipv6_dialog():
|
||||||
"по IPv6. Если данная мера не помогает, попробуйте отключить IPv6 "
|
"по IPv6. Если данная мера не помогает, попробуйте отключить IPv6 "
|
||||||
"в системе.\n\n"
|
"в системе.\n\n"
|
||||||
"Это предупреждение будет показано только один раз.",
|
"Это предупреждение будет показано только один раз.",
|
||||||
"TG WS Proxy")
|
"TG WS Proxy",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _build_menu():
|
def _build_menu():
|
||||||
|
|
@ -672,9 +776,8 @@ def _build_menu():
|
||||||
port = _config.get("port", DEFAULT_CONFIG["port"])
|
port = _config.get("port", DEFAULT_CONFIG["port"])
|
||||||
return pystray.Menu(
|
return pystray.Menu(
|
||||||
pystray.MenuItem(
|
pystray.MenuItem(
|
||||||
f"Открыть в Telegram ({host}:{port})",
|
f"Открыть в Telegram ({host}:{port})", _on_open_in_telegram, default=True
|
||||||
_on_open_in_telegram,
|
),
|
||||||
default=True),
|
|
||||||
pystray.Menu.SEPARATOR,
|
pystray.Menu.SEPARATOR,
|
||||||
pystray.MenuItem("Перезапустить прокси", _on_restart),
|
pystray.MenuItem("Перезапустить прокси", _on_restart),
|
||||||
pystray.MenuItem("Настройки...", _on_edit_config),
|
pystray.MenuItem("Настройки...", _on_edit_config),
|
||||||
|
|
@ -702,8 +805,7 @@ def run_tray():
|
||||||
log.info("Log file: %s", LOG_FILE)
|
log.info("Log file: %s", LOG_FILE)
|
||||||
|
|
||||||
if pystray is None or Image is None:
|
if pystray is None or Image is None:
|
||||||
log.error("pystray or Pillow not installed; "
|
log.error("pystray or Pillow not installed; running in console mode")
|
||||||
"running in console mode")
|
|
||||||
start_proxy()
|
start_proxy()
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
|
|
@ -718,11 +820,7 @@ def run_tray():
|
||||||
_check_ipv6_warning()
|
_check_ipv6_warning()
|
||||||
|
|
||||||
icon_image = _load_icon()
|
icon_image = _load_icon()
|
||||||
_tray_icon = pystray.Icon(
|
_tray_icon = pystray.Icon(APP_NAME, icon_image, "TG WS Proxy", menu=_build_menu())
|
||||||
APP_NAME,
|
|
||||||
icon_image,
|
|
||||||
"TG WS Proxy",
|
|
||||||
menu=_build_menu())
|
|
||||||
|
|
||||||
log.info("Tray icon running")
|
log.info("Tray icon running")
|
||||||
_tray_icon.run()
|
_tray_icon.run()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue