Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4d777113c | ||
|
|
26926f6469 | ||
|
|
a0e2600399 |
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "999909181706",
|
"version": "999909182137",
|
||||||
"pac_base_url": "https://cdn.rrrosmontis.icu/RRPP/PAC/",
|
"pac_base_url": "https://cdn.rrrosmontis.icu/RRPP/PAC/",
|
||||||
"nodes_url": "https://api.rrrosmontis.icu/v1/rrpp/subscription?method=rrpp_client",
|
"nodes_url": "https://api.rrrosmontis.icu/v1/rrpp/subscription?method=rrpp_client",
|
||||||
"update_url": "https://git.rrrosmontis.icu/api/v1/repos/RoyirinRosmontis/Private-Proxy/releases/latest"
|
"update_url": "https://git.rrrosmontis.icu/api/v1/repos/RoyirinRosmontis/Private-Proxy/releases/latest"
|
||||||
|
|||||||
+166
-12
@@ -7,7 +7,7 @@ from pathlib import Path
|
|||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QApplication, QWidget, QLabel, QPushButton,
|
QApplication, QWidget, QLabel, QPushButton,
|
||||||
QVBoxLayout, QHBoxLayout, QFrame, QMessageBox, QTextEdit,
|
QVBoxLayout, QHBoxLayout, QFrame, QMessageBox, QTextEdit,
|
||||||
QCheckBox, QMenu, QDialog, QProgressDialog,
|
QCheckBox, QMenu, QDialog, QProgressDialog, QAction, QSystemTrayIcon,
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import Qt, QTimer, pyqtSignal
|
from PyQt5.QtCore import Qt, QTimer, pyqtSignal
|
||||||
from PyQt5.QtGui import QFont, QIcon, QPixmap, QImage, QColor
|
from PyQt5.QtGui import QFont, QIcon, QPixmap, QImage, QColor
|
||||||
@@ -816,7 +816,9 @@ def _endpoint_label():
|
|||||||
domain = _endpoint_domain()
|
domain = _endpoint_domain()
|
||||||
if domain == "???":
|
if domain == "???":
|
||||||
return "无可用节点(请登录)"
|
return "无可用节点(请登录)"
|
||||||
return f"{domain} ({_endpoint_country()})"
|
display = _node_display_mode()
|
||||||
|
name = str(node.get("remark") or "").strip() if display == "remark" else ""
|
||||||
|
return f"{name or domain} ({_endpoint_country()})"
|
||||||
|
|
||||||
def _proxy_auth_basic(node=None):
|
def _proxy_auth_basic(node=None):
|
||||||
"""正向代理 Basic 认证凭证:methods.http.{username,password}(新格式)
|
"""正向代理 Basic 认证凭证:methods.http.{username,password}(新格式)
|
||||||
@@ -1368,6 +1370,24 @@ def _set_local_port(port):
|
|||||||
_settings_save(data)
|
_settings_save(data)
|
||||||
log(f"代理转发端口已保存: {port}(下次开启代理生效)")
|
log(f"代理转发端口已保存: {port}(下次开启代理生效)")
|
||||||
|
|
||||||
|
def _use_builtin_pac():
|
||||||
|
"""是否使用固定本地代理承载系统流量、由转发器内部执行 PAC 判断。"""
|
||||||
|
return bool(_settings_load().get("builtin_pac", True))
|
||||||
|
|
||||||
|
def _set_builtin_pac(enabled):
|
||||||
|
"""保存内置 PAC 偏好,下一次开启代理时生效。"""
|
||||||
|
data = _settings_load()
|
||||||
|
data["builtin_pac"] = bool(enabled)
|
||||||
|
_settings_save(data)
|
||||||
|
|
||||||
|
def _should_use_system_pac(use_pac, use_builtin_pac):
|
||||||
|
"""决定系统侧是否下发 PAC;内置 PAC 只保留转发器内部智能分流。"""
|
||||||
|
return bool(use_pac and not use_builtin_pac)
|
||||||
|
|
||||||
|
def _should_use_internal_pac(use_pac, use_builtin_pac):
|
||||||
|
"""决定转发器内部是否启用 PAC 智能直连/代理判断。"""
|
||||||
|
return bool(use_pac or use_builtin_pac)
|
||||||
|
|
||||||
def _update_via_proxy():
|
def _update_via_proxy():
|
||||||
"""更新检查/下载是否走节点隧道(CONNECT);设置项,默认关=直连仓库。"""
|
"""更新检查/下载是否走节点隧道(CONNECT);设置项,默认关=直连仓库。"""
|
||||||
return bool(_settings_load().get("update_via_proxy", False))
|
return bool(_settings_load().get("update_via_proxy", False))
|
||||||
@@ -1415,6 +1435,47 @@ def _set_bypass_lan(enabled):
|
|||||||
data["bypass_lan"] = bool(enabled)
|
data["bypass_lan"] = bool(enabled)
|
||||||
_settings_save(data)
|
_settings_save(data)
|
||||||
|
|
||||||
|
|
||||||
|
def _node_display_mode():
|
||||||
|
"""主页面节点信息显示方式:domain 或 remark。"""
|
||||||
|
return "remark" if _settings_load().get("node_display") == "remark" else "domain"
|
||||||
|
|
||||||
|
|
||||||
|
def _set_node_display_mode(mode):
|
||||||
|
"""保存主页面节点信息显示方式。"""
|
||||||
|
data = _settings_load()
|
||||||
|
data["node_display"] = "remark" if mode == "remark" else "domain"
|
||||||
|
_settings_save(data)
|
||||||
|
|
||||||
|
|
||||||
|
def _close_behavior():
|
||||||
|
"""窗口关闭行为:exit(退出)或 tray(最小化到托盘)。"""
|
||||||
|
return "tray" if _settings_load().get("close_behavior") == "tray" else "exit"
|
||||||
|
|
||||||
|
|
||||||
|
def _set_close_behavior(mode):
|
||||||
|
"""保存窗口关闭行为。"""
|
||||||
|
data = _settings_load()
|
||||||
|
data["close_behavior"] = "tray" if mode == "tray" else "exit"
|
||||||
|
_settings_save(data)
|
||||||
|
|
||||||
|
|
||||||
|
def _reset_user_config():
|
||||||
|
"""重置 settings.json;保留登录、节点缓存、第三方订阅和 PAC 数据。"""
|
||||||
|
global _WHITELIST, _ROUTE_RULES, _WHITELIST_VERSION, _NODES, _SELECTED_NODE
|
||||||
|
_settings_save({})
|
||||||
|
_WHITELIST = None
|
||||||
|
_ROUTE_RULES = None
|
||||||
|
_WHITELIST_VERSION += 1
|
||||||
|
_is_whitelisted.cache_clear()
|
||||||
|
_NODES = None
|
||||||
|
_SELECTED_NODE = 0
|
||||||
|
default_port = _port_val()
|
||||||
|
global _TLS_LOCAL_PORT, PROXY_PORT
|
||||||
|
_TLS_LOCAL_PORT = default_port
|
||||||
|
PROXY_PORT = default_port
|
||||||
|
log("已重置用户配置:本地偏好、域名路由、跳板和当前节点选择已恢复默认")
|
||||||
|
|
||||||
# ── 域名路由(兼容旧代理白名单,settings.json 的 whitelist 键) ────────
|
# ── 域名路由(兼容旧代理白名单,settings.json 的 whitelist 键) ────────
|
||||||
# 每条规则 {"value": 域名/IP/CIDR, "enabled": 是否启用,
|
# 每条规则 {"value": 域名/IP/CIDR, "enabled": 是否启用,
|
||||||
# "action": "direct" | "node", "node_ref": 节点稳定标识}。
|
# "action": "direct" | "node", "node_ref": 节点稳定标识}。
|
||||||
@@ -2612,12 +2673,19 @@ def _settings_services():
|
|||||||
load_icon=load_icon,
|
load_icon=load_icon,
|
||||||
version=VERSION,
|
version=VERSION,
|
||||||
get_port=lambda: _TLS_LOCAL_PORT,
|
get_port=lambda: _TLS_LOCAL_PORT,
|
||||||
|
get_builtin_pac=_use_builtin_pac,
|
||||||
get_update_via_proxy=_update_via_proxy,
|
get_update_via_proxy=_update_via_proxy,
|
||||||
get_skip_update_check=_skip_update_check,
|
get_skip_update_check=_skip_update_check,
|
||||||
|
get_node_display_mode=_node_display_mode,
|
||||||
|
get_close_behavior=_close_behavior,
|
||||||
parse_port=_parse_port,
|
parse_port=_parse_port,
|
||||||
set_port=_set_local_port,
|
set_port=_set_local_port,
|
||||||
|
set_builtin_pac=_set_builtin_pac,
|
||||||
set_update_via_proxy=_set_update_via_proxy,
|
set_update_via_proxy=_set_update_via_proxy,
|
||||||
set_skip_update_check=_set_skip_update_check,
|
set_skip_update_check=_set_skip_update_check,
|
||||||
|
set_node_display_mode=_set_node_display_mode,
|
||||||
|
set_close_behavior=_set_close_behavior,
|
||||||
|
reset_user_config=_reset_user_config,
|
||||||
open_whitelist=lambda parent: FixedRouteDialog(_fixed_route_services(), parent).exec_(),
|
open_whitelist=lambda parent: FixedRouteDialog(_fixed_route_services(), parent).exec_(),
|
||||||
open_jump=lambda parent: JumpDialog(_jump_services(), parent).exec_(),
|
open_jump=lambda parent: JumpDialog(_jump_services(), parent).exec_(),
|
||||||
open_third_party=lambda parent: ThirdPartyDialog(_third_party_services(), parent).exec_(),
|
open_third_party=lambda parent: ThirdPartyDialog(_third_party_services(), parent).exec_(),
|
||||||
@@ -2654,6 +2722,9 @@ class MainWindow(QWidget):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._proxy_set_by_us = False # 系统代理是否由本程序开启(退出时只清理自己设置的)
|
self._proxy_set_by_us = False # 系统代理是否由本程序开启(退出时只清理自己设置的)
|
||||||
|
self._quitting = False
|
||||||
|
self._tray = None
|
||||||
|
self._tray_menu = None
|
||||||
# 启动完成前仍允许用户取消。事件由 GUI 线程置位,后台启动线程只读取;
|
# 启动完成前仍允许用户取消。事件由 GUI 线程置位,后台启动线程只读取;
|
||||||
# 锁保护系统代理写入和取消清理之间的交接,避免遗留代理。
|
# 锁保护系统代理写入和取消清理之间的交接,避免遗留代理。
|
||||||
self._start_cancel_event = None
|
self._start_cancel_event = None
|
||||||
@@ -2675,6 +2746,7 @@ class MainWindow(QWidget):
|
|||||||
_user_init_local() # 首启初始化用户数据目录(节点模板),须在 init_ui 读节点之前
|
_user_init_local() # 首启初始化用户数据目录(节点模板),须在 init_ui 读节点之前
|
||||||
|
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
|
self._setup_tray()
|
||||||
|
|
||||||
# 每 2 秒刷新一次状态指示灯
|
# 每 2 秒刷新一次状态指示灯
|
||||||
self.timer = QTimer()
|
self.timer = QTimer()
|
||||||
@@ -2694,6 +2766,65 @@ class MainWindow(QWidget):
|
|||||||
# 节点未配置时延迟弹出登录框(等窗口显示后)
|
# 节点未配置时延迟弹出登录框(等窗口显示后)
|
||||||
QTimer.singleShot(600, self._maybe_show_login)
|
QTimer.singleShot(600, self._maybe_show_login)
|
||||||
|
|
||||||
|
def _setup_tray(self):
|
||||||
|
"""创建托盘图标和动态状态菜单;系统不支持托盘时保留窗口模式。"""
|
||||||
|
if not QSystemTrayIcon.isSystemTrayAvailable():
|
||||||
|
log("当前系统不支持托盘图标,关闭窗口时只能退出程序")
|
||||||
|
return
|
||||||
|
self._tray = QSystemTrayIcon(load_icon(), self)
|
||||||
|
self._tray.setToolTip("Royirin Rosmontis Private Proxy")
|
||||||
|
self._tray_menu = QMenu(self)
|
||||||
|
self._tray_menu.aboutToShow.connect(self._update_tray_menu)
|
||||||
|
self._tray.setContextMenu(self._tray_menu)
|
||||||
|
self._tray.activated.connect(self._on_tray_activated)
|
||||||
|
self._tray.show()
|
||||||
|
self._update_tray_menu()
|
||||||
|
|
||||||
|
def _on_tray_activated(self, reason):
|
||||||
|
"""双击托盘图标显示主页面。"""
|
||||||
|
if reason == QSystemTrayIcon.DoubleClick:
|
||||||
|
self._show_main_window()
|
||||||
|
|
||||||
|
def _show_main_window(self):
|
||||||
|
"""从托盘恢复主页面并置于前台。"""
|
||||||
|
self.show()
|
||||||
|
self.showNormal()
|
||||||
|
self.raise_()
|
||||||
|
self.activateWindow()
|
||||||
|
|
||||||
|
def _update_tray_menu(self):
|
||||||
|
"""刷新托盘菜单中的三行状态信息。"""
|
||||||
|
if self._tray_menu is None:
|
||||||
|
return
|
||||||
|
self._tray_menu.clear()
|
||||||
|
|
||||||
|
show_action = QAction("显示主页面", self._tray_menu)
|
||||||
|
show_action.triggered.connect(self._show_main_window)
|
||||||
|
self._tray_menu.addAction(show_action)
|
||||||
|
self._tray_menu.addSeparator()
|
||||||
|
|
||||||
|
proxy_on = is_proxy_on()
|
||||||
|
pac_on = _should_use_internal_pac(_use_pac_mode(), _use_builtin_pac())
|
||||||
|
bypass_lan = _bypass_lan()
|
||||||
|
for text in (
|
||||||
|
f"代理:{'开' if proxy_on else '关'}",
|
||||||
|
f"PAC:{'启用' if pac_on else '关闭'}",
|
||||||
|
f"内网IP:{'不代理' if bypass_lan else '代理'}",
|
||||||
|
):
|
||||||
|
status_action = QAction(text, self._tray_menu)
|
||||||
|
status_action.setEnabled(False)
|
||||||
|
self._tray_menu.addAction(status_action)
|
||||||
|
|
||||||
|
self._tray_menu.addSeparator()
|
||||||
|
exit_action = QAction("退出", self._tray_menu)
|
||||||
|
exit_action.triggered.connect(self._exit_from_tray)
|
||||||
|
self._tray_menu.addAction(exit_action)
|
||||||
|
|
||||||
|
def _exit_from_tray(self):
|
||||||
|
"""托盘菜单中的显式退出。"""
|
||||||
|
self._quitting = True
|
||||||
|
self.close()
|
||||||
|
|
||||||
def _verify_node_thread(self):
|
def _verify_node_thread(self):
|
||||||
"""等节点配置在线同步完成后,校验当前选中节点是否可用,必要时自动回退。
|
"""等节点配置在线同步完成后,校验当前选中节点是否可用,必要时自动回退。
|
||||||
|
|
||||||
@@ -2884,7 +3015,11 @@ class MainWindow(QWidget):
|
|||||||
layout.addWidget(hints)
|
layout.addWidget(hints)
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
"""关闭窗口时自动清理代理和隧道。"""
|
"""按用户设置退出或隐藏到托盘。"""
|
||||||
|
if not self._quitting and _close_behavior() == "tray" and self._tray is not None:
|
||||||
|
event.ignore()
|
||||||
|
self.hide()
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
self.quit_app()
|
self.quit_app()
|
||||||
finally:
|
finally:
|
||||||
@@ -2954,6 +3089,7 @@ class MainWindow(QWidget):
|
|||||||
# 在 GUI 线程读取控件状态传参,避免子线程直接访问 Qt 控件(非线程安全)
|
# 在 GUI 线程读取控件状态传参,避免子线程直接访问 Qt 控件(非线程安全)
|
||||||
use_pac = self.cb_pac.isChecked()
|
use_pac = self.cb_pac.isChecked()
|
||||||
use_bypass_lan = self.cb_bypass_lan.isChecked()
|
use_bypass_lan = self.cb_bypass_lan.isChecked()
|
||||||
|
use_builtin_pac = _use_builtin_pac()
|
||||||
self._start_cancel_event = threading.Event()
|
self._start_cancel_event = threading.Event()
|
||||||
self._start_in_progress = True
|
self._start_in_progress = True
|
||||||
# 网络探测可能需要数秒;此时保留按钮给用户取消,而不是把界面锁死。
|
# 网络探测可能需要数秒;此时保留按钮给用户取消,而不是把界面锁死。
|
||||||
@@ -2961,12 +3097,12 @@ class MainWindow(QWidget):
|
|||||||
self.btn_toggle.setEnabled(True)
|
self.btn_toggle.setEnabled(True)
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=self._start_thread,
|
target=self._start_thread,
|
||||||
args=(use_pac, use_bypass_lan, self._start_cancel_event),
|
args=(use_pac, use_bypass_lan, use_builtin_pac, self._start_cancel_event),
|
||||||
daemon=True,
|
daemon=True,
|
||||||
).start()
|
).start()
|
||||||
def _start_thread(self, use_pac, use_bypass_lan, cancel_event):
|
def _start_thread(self, use_pac, use_bypass_lan, use_builtin_pac, cancel_event):
|
||||||
try:
|
try:
|
||||||
self._start_proxy(use_pac, use_bypass_lan, cancel_event)
|
self._start_proxy(use_pac, use_bypass_lan, use_builtin_pac, cancel_event)
|
||||||
_raise_if_start_cancelled(cancel_event)
|
_raise_if_start_cancelled(cancel_event)
|
||||||
self._op_done.emit("start", True)
|
self._op_done.emit("start", True)
|
||||||
except _StartCancelled:
|
except _StartCancelled:
|
||||||
@@ -2991,8 +3127,8 @@ class MainWindow(QWidget):
|
|||||||
_tls_stop()
|
_tls_stop()
|
||||||
_stop_pac_server()
|
_stop_pac_server()
|
||||||
|
|
||||||
def _start_proxy(self, use_pac, use_bypass_lan, cancel_event=None):
|
def _start_proxy(self, use_pac, use_bypass_lan, use_builtin_pac=False, cancel_event=None):
|
||||||
"""启动本地转发器并设置系统代理(PAC/固定两种模式),可在后台线程调用。"""
|
"""启动本地转发器并设置系统代理(普通 PAC、内置 PAC 或固定代理)。"""
|
||||||
if _endpoint_domain() == "???":
|
if _endpoint_domain() == "???":
|
||||||
# 未配置节点时禁止开启:否则系统代理会指向连不通任何目标的转发器,导致断网
|
# 未配置节点时禁止开启:否则系统代理会指向连不通任何目标的转发器,导致断网
|
||||||
raise RuntimeError("未配置节点:请先通过 nodes_url 在线更新或 nodes_tool 导入节点配置")
|
raise RuntimeError("未配置节点:请先通过 nodes_url 在线更新或 nodes_tool 导入节点配置")
|
||||||
@@ -3008,16 +3144,18 @@ class MainWindow(QWidget):
|
|||||||
if jump_chain:
|
if jump_chain:
|
||||||
log(f" 跳板路径: {_jump_route_text(current_node)}")
|
log(f" 跳板路径: {_jump_route_text(current_node)}")
|
||||||
# PAC 模式:全部流量进转发器,按域名表/解析 IP 智能分流
|
# PAC 模式:全部流量进转发器,按域名表/解析 IP 智能分流
|
||||||
if use_pac:
|
use_internal_pac = _should_use_internal_pac(use_pac, use_builtin_pac)
|
||||||
|
if use_internal_pac:
|
||||||
_load_cn_ranges()
|
_load_cn_ranges()
|
||||||
_load_force_proxy_suffixes()
|
_load_force_proxy_suffixes()
|
||||||
_load_direct_domains()
|
_load_direct_domains()
|
||||||
_raise_if_start_cancelled(cancel_event)
|
_raise_if_start_cancelled(cancel_event)
|
||||||
_tls_start(ep, _endpoint_port(), use_tls=True, bypass_lan=use_bypass_lan, smart=use_pac,
|
_tls_start(ep, _endpoint_port(), use_tls=True, bypass_lan=use_bypass_lan,
|
||||||
|
smart=use_internal_pac,
|
||||||
jump_chain=jump_chain)
|
jump_chain=jump_chain)
|
||||||
try:
|
try:
|
||||||
_raise_if_start_cancelled(cancel_event)
|
_raise_if_start_cancelled(cancel_event)
|
||||||
if use_pac:
|
if _should_use_system_pac(use_pac, use_builtin_pac):
|
||||||
# 本地生成全量转发 PAC(不再从服务器拉取),全部流量交给转发器做智能分流
|
# 本地生成全量转发 PAC(不再从服务器拉取),全部流量交给转发器做智能分流
|
||||||
pac_content = _build_pac(f"{PROXY_HOST}:{PROXY_PORT}")
|
pac_content = _build_pac(f"{PROXY_HOST}:{PROXY_PORT}")
|
||||||
if use_bypass_lan:
|
if use_bypass_lan:
|
||||||
@@ -3035,6 +3173,8 @@ class MainWindow(QWidget):
|
|||||||
_raise_if_start_cancelled(cancel_event)
|
_raise_if_start_cancelled(cancel_event)
|
||||||
proxy_on(bypass_lan=use_bypass_lan)
|
proxy_on(bypass_lan=use_bypass_lan)
|
||||||
self._proxy_set_by_us = True
|
self._proxy_set_by_us = True
|
||||||
|
if use_builtin_pac:
|
||||||
|
log(f"使用内置 PAC:系统固定代理 127.0.0.1:{PROXY_PORT},转发器内部智能分流")
|
||||||
_raise_if_start_cancelled(cancel_event)
|
_raise_if_start_cancelled(cancel_event)
|
||||||
if use_bypass_lan:
|
if use_bypass_lan:
|
||||||
log("不代理内网 IP:内网地址将直连")
|
log("不代理内网 IP:内网地址将直连")
|
||||||
@@ -3089,8 +3229,15 @@ class MainWindow(QWidget):
|
|||||||
def _show_settings_dialog(self):
|
def _show_settings_dialog(self):
|
||||||
"""打开设置弹窗;第三方订阅与登录状态可独立管理。"""
|
"""打开设置弹窗;第三方订阅与登录状态可独立管理。"""
|
||||||
SettingsDialog(_settings_services(), self, port_locked=self._proxy_set_by_us).exec_()
|
SettingsDialog(_settings_services(), self, port_locked=self._proxy_set_by_us).exec_()
|
||||||
|
self._refresh_preferences()
|
||||||
self._refresh_subtitle()
|
self._refresh_subtitle()
|
||||||
|
|
||||||
|
def _refresh_preferences(self):
|
||||||
|
"""设置页关闭后同步主页面的本地偏好显示。"""
|
||||||
|
self.cb_pac.setChecked(_use_pac_mode())
|
||||||
|
self.cb_bypass_lan.setChecked(_bypass_lan())
|
||||||
|
self._update_tray_menu()
|
||||||
|
|
||||||
# ── 节点登录 ──
|
# ── 节点登录 ──
|
||||||
def _maybe_show_login(self):
|
def _maybe_show_login(self):
|
||||||
"""首次无节点时提示登录;用户可跳过并在主界面添加第三方订阅。"""
|
"""首次无节点时提示登录;用户可跳过并在主界面添加第三方订阅。"""
|
||||||
@@ -3181,6 +3328,7 @@ class MainWindow(QWidget):
|
|||||||
def _refresh_subtitle(self):
|
def _refresh_subtitle(self):
|
||||||
"""刷新节点副标题文字。"""
|
"""刷新节点副标题文字。"""
|
||||||
self.subtitle.setText(_endpoint_label())
|
self.subtitle.setText(_endpoint_label())
|
||||||
|
self._update_tray_menu()
|
||||||
|
|
||||||
def _update_node_label(self):
|
def _update_node_label(self):
|
||||||
"""代理运行中时提示禁止切换节点。"""
|
"""代理运行中时提示禁止切换节点。"""
|
||||||
@@ -3274,6 +3422,7 @@ class MainWindow(QWidget):
|
|||||||
self.traffic_up.setText(f"↑{_fmt_bytes(up_rate)}/s")
|
self.traffic_up.setText(f"↑{_fmt_bytes(up_rate)}/s")
|
||||||
self.traffic_down.setText(f"↓{_fmt_bytes(down_rate)}/s")
|
self.traffic_down.setText(f"↓{_fmt_bytes(down_rate)}/s")
|
||||||
self.traffic_total.setText(_fmt_bytes(up_total + down_total))
|
self.traffic_total.setText(_fmt_bytes(up_total + down_total))
|
||||||
|
self._update_tray_menu()
|
||||||
def _check_update(self, manual=False):
|
def _check_update(self, manual=False):
|
||||||
"""后台检查服务器是否有新版本;manual=True 时无视「跳过检查更新」设置。"""
|
"""后台检查服务器是否有新版本;manual=True 时无视「跳过检查更新」设置。"""
|
||||||
# 先初始化结果,任何路径(含跳过)完成后 _on_update_done 都能安全读取
|
# 先初始化结果,任何路径(含跳过)完成后 _on_update_done 都能安全读取
|
||||||
@@ -3490,6 +3639,9 @@ class MainWindow(QWidget):
|
|||||||
|
|
||||||
本程序异常退出残留的代理设置由下次启动时 _cleanup_stale_proxy() 处理。
|
本程序异常退出残留的代理设置由下次启动时 _cleanup_stale_proxy() 处理。
|
||||||
"""
|
"""
|
||||||
|
self._quitting = True
|
||||||
|
if self._tray is not None:
|
||||||
|
self._tray.hide()
|
||||||
if self._start_in_progress and self._start_cancel_event is not None:
|
if self._start_in_progress and self._start_cancel_event is not None:
|
||||||
self._start_cancel_event.set()
|
self._start_cancel_event.set()
|
||||||
try:
|
try:
|
||||||
@@ -3583,7 +3735,9 @@ def main():
|
|||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
icon = load_icon()
|
icon = load_icon()
|
||||||
app.setWindowIcon(icon)
|
app.setWindowIcon(icon)
|
||||||
app.setQuitOnLastWindowClosed(True)
|
# 主窗口可按设置隐藏到托盘;显式点击托盘“退出”或关闭行为为“退出”时,
|
||||||
|
# MainWindow.quit_app() 会负责结束 QApplication。
|
||||||
|
app.setQuitOnLastWindowClosed(False)
|
||||||
|
|
||||||
window = MainWindow()
|
window = MainWindow()
|
||||||
window.show()
|
window.show()
|
||||||
|
|||||||
+123
-4
@@ -9,6 +9,7 @@ from PyQt5.QtCore import Qt, QTimer, pyqtSignal
|
|||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QApplication, QComboBox, QDialog, QFileDialog, QHBoxLayout, QLabel, QLineEdit,
|
QApplication, QComboBox, QDialog, QFileDialog, QHBoxLayout, QLabel, QLineEdit,
|
||||||
QListWidget, QListWidgetItem, QPushButton, QVBoxLayout, QWidget, QTextEdit,
|
QListWidget, QListWidgetItem, QPushButton, QVBoxLayout, QWidget, QTextEdit,
|
||||||
|
QButtonGroup, QMessageBox,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .theme import C_BG, C_BORDER, C_CARD, C_GREEN, C_RED, C_SUBTEXT, C_TEXT, C_TITLE
|
from .theme import C_BG, C_BORDER, C_CARD, C_GREEN, C_RED, C_SUBTEXT, C_TEXT, C_TITLE
|
||||||
@@ -930,12 +931,19 @@ class SettingsServices:
|
|||||||
load_icon: object
|
load_icon: object
|
||||||
version: str
|
version: str
|
||||||
get_port: object
|
get_port: object
|
||||||
|
get_builtin_pac: object
|
||||||
get_update_via_proxy: object
|
get_update_via_proxy: object
|
||||||
get_skip_update_check: object
|
get_skip_update_check: object
|
||||||
|
get_node_display_mode: object
|
||||||
|
get_close_behavior: object
|
||||||
parse_port: object
|
parse_port: object
|
||||||
set_port: object
|
set_port: object
|
||||||
|
set_builtin_pac: object
|
||||||
set_update_via_proxy: object
|
set_update_via_proxy: object
|
||||||
set_skip_update_check: object
|
set_skip_update_check: object
|
||||||
|
set_node_display_mode: object
|
||||||
|
set_close_behavior: object
|
||||||
|
reset_user_config: object
|
||||||
open_whitelist: object
|
open_whitelist: object
|
||||||
open_jump: object
|
open_jump: object
|
||||||
open_third_party: object
|
open_third_party: object
|
||||||
@@ -953,7 +961,7 @@ class SettingsDialog(QDialog):
|
|||||||
self._services = services
|
self._services = services
|
||||||
self.setWindowTitle("设置")
|
self.setWindowTitle("设置")
|
||||||
self.setWindowIcon(services.load_icon())
|
self.setWindowIcon(services.load_icon())
|
||||||
self.setFixedSize(300, 350)
|
self.setFixedSize(300, 470)
|
||||||
self.setStyleSheet(f"""
|
self.setStyleSheet(f"""
|
||||||
QDialog {{ background-color: {C_BG}; }}
|
QDialog {{ background-color: {C_BG}; }}
|
||||||
QLabel {{ color: {C_TEXT}; font-family: "Microsoft YaHei", "Segoe UI", sans-serif; font-size: 12px; }}
|
QLabel {{ color: {C_TEXT}; font-family: "Microsoft YaHei", "Segoe UI", sans-serif; font-size: 12px; }}
|
||||||
@@ -961,8 +969,8 @@ class SettingsDialog(QDialog):
|
|||||||
padding: 2px 8px; color: {C_TEXT}; font-size: 12px; }}
|
padding: 2px 8px; color: {C_TEXT}; font-size: 12px; }}
|
||||||
""")
|
""")
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
layout.setSpacing(10)
|
layout.setSpacing(7)
|
||||||
layout.setContentsMargins(20, 10, 20, 10)
|
layout.setContentsMargins(20, 8, 20, 8)
|
||||||
title = QLabel("设置")
|
title = QLabel("设置")
|
||||||
title.setStyleSheet(f"color: {C_TITLE}; font-size: 15px; font-weight: bold;")
|
title.setStyleSheet(f"color: {C_TITLE}; font-size: 15px; font-weight: bold;")
|
||||||
title.setAlignment(Qt.AlignCenter)
|
title.setAlignment(Qt.AlignCenter)
|
||||||
@@ -989,7 +997,7 @@ class SettingsDialog(QDialog):
|
|||||||
self.lb_hint.setFixedHeight(14)
|
self.lb_hint.setFixedHeight(14)
|
||||||
self.lb_hint.setAlignment(Qt.AlignCenter)
|
self.lb_hint.setAlignment(Qt.AlignCenter)
|
||||||
layout.addWidget(self.lb_hint)
|
layout.addWidget(self.lb_hint)
|
||||||
layout.addStretch()
|
layout.addSpacing(2)
|
||||||
|
|
||||||
whitelist_row = QHBoxLayout()
|
whitelist_row = QHBoxLayout()
|
||||||
whitelist_label = QLabel("域名路由")
|
whitelist_label = QLabel("域名路由")
|
||||||
@@ -1056,6 +1064,21 @@ class SettingsDialog(QDialog):
|
|||||||
port_row.addWidget(self.ed_port)
|
port_row.addWidget(self.ed_port)
|
||||||
layout.addLayout(port_row)
|
layout.addLayout(port_row)
|
||||||
|
|
||||||
|
builtin_pac_row = QHBoxLayout()
|
||||||
|
builtin_pac_label = QLabel("使用内置 PAC")
|
||||||
|
builtin_pac_label.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 12px;")
|
||||||
|
builtin_pac_label.setToolTip(
|
||||||
|
"系统使用固定的本地代理端口;转发器内部仍按 PAC 规则智能判断直连或代理"
|
||||||
|
)
|
||||||
|
builtin_pac_row.addWidget(builtin_pac_label)
|
||||||
|
builtin_pac_row.addStretch()
|
||||||
|
self.cb_builtin_pac = ToggleSwitch(checked=services.get_builtin_pac())
|
||||||
|
self.cb_builtin_pac.setToolTip(
|
||||||
|
"系统固定指向本地代理端口,转发器内部保留 PAC 智能分流"
|
||||||
|
)
|
||||||
|
builtin_pac_row.addWidget(self.cb_builtin_pac)
|
||||||
|
layout.addLayout(builtin_pac_row)
|
||||||
|
|
||||||
update_row = QHBoxLayout()
|
update_row = QHBoxLayout()
|
||||||
update_label = QLabel("使用代理更新")
|
update_label = QLabel("使用代理更新")
|
||||||
update_label.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 12px;")
|
update_label.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 12px;")
|
||||||
@@ -1075,6 +1098,74 @@ class SettingsDialog(QDialog):
|
|||||||
self.cb_skip_update.setToolTip("跳过 PAC 数据与程序版本检查(节点更新与登录不受影响),被跳过的检查不输出日志")
|
self.cb_skip_update.setToolTip("跳过 PAC 数据与程序版本检查(节点更新与登录不受影响),被跳过的检查不输出日志")
|
||||||
skip_row.addWidget(self.cb_skip_update)
|
skip_row.addWidget(self.cb_skip_update)
|
||||||
layout.addLayout(skip_row)
|
layout.addLayout(skip_row)
|
||||||
|
|
||||||
|
option_style = f"""
|
||||||
|
QPushButton {{ background-color: {C_CARD}; color: {C_SUBTEXT}; border: 1px solid {C_BORDER};
|
||||||
|
border-radius: 6px; padding: 2px 8px; font-size: 11px; min-width: 32px; }}
|
||||||
|
QPushButton:hover {{ background-color: #e3e8ee; color: {C_TEXT}; }}
|
||||||
|
QPushButton:checked {{ background-color: {C_TITLE}; color: #ffffff; border-color: {C_TITLE}; }}
|
||||||
|
"""
|
||||||
|
|
||||||
|
node_display_row = QHBoxLayout()
|
||||||
|
node_display_label = QLabel("节点信息显示")
|
||||||
|
node_display_label.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 12px;")
|
||||||
|
node_display_label.setToolTip("控制主页面节点副标题显示域名还是节点备注")
|
||||||
|
node_display_row.addWidget(node_display_label)
|
||||||
|
node_display_row.addStretch()
|
||||||
|
self.btn_node_domain = QPushButton("域名")
|
||||||
|
self.btn_node_remark = QPushButton("备注")
|
||||||
|
for button in (self.btn_node_domain, self.btn_node_remark):
|
||||||
|
button.setCheckable(True)
|
||||||
|
button.setCursor(Qt.PointingHandCursor)
|
||||||
|
button.setStyleSheet(option_style)
|
||||||
|
node_display_row.addWidget(button)
|
||||||
|
self.node_display_group = QButtonGroup(self)
|
||||||
|
self.node_display_group.setExclusive(True)
|
||||||
|
self.node_display_group.addButton(self.btn_node_domain)
|
||||||
|
self.node_display_group.addButton(self.btn_node_remark)
|
||||||
|
if services.get_node_display_mode() == "remark":
|
||||||
|
self.btn_node_remark.setChecked(True)
|
||||||
|
else:
|
||||||
|
self.btn_node_domain.setChecked(True)
|
||||||
|
layout.addLayout(node_display_row)
|
||||||
|
|
||||||
|
close_behavior_row = QHBoxLayout()
|
||||||
|
close_behavior_label = QLabel("默认关闭行为")
|
||||||
|
close_behavior_label.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 12px;")
|
||||||
|
close_behavior_label.setToolTip("点击主窗口关闭按钮时退出程序,或隐藏到系统托盘")
|
||||||
|
close_behavior_row.addWidget(close_behavior_label)
|
||||||
|
close_behavior_row.addStretch()
|
||||||
|
self.btn_close_exit = QPushButton("退出")
|
||||||
|
self.btn_close_tray = QPushButton("托盘")
|
||||||
|
for button in (self.btn_close_exit, self.btn_close_tray):
|
||||||
|
button.setCheckable(True)
|
||||||
|
button.setCursor(Qt.PointingHandCursor)
|
||||||
|
button.setStyleSheet(option_style)
|
||||||
|
close_behavior_row.addWidget(button)
|
||||||
|
self.close_behavior_group = QButtonGroup(self)
|
||||||
|
self.close_behavior_group.setExclusive(True)
|
||||||
|
self.close_behavior_group.addButton(self.btn_close_exit)
|
||||||
|
self.close_behavior_group.addButton(self.btn_close_tray)
|
||||||
|
if services.get_close_behavior() == "tray":
|
||||||
|
self.btn_close_tray.setChecked(True)
|
||||||
|
else:
|
||||||
|
self.btn_close_exit.setChecked(True)
|
||||||
|
layout.addLayout(close_behavior_row)
|
||||||
|
|
||||||
|
reset_row = QHBoxLayout()
|
||||||
|
reset_label = QLabel("重置用户配置")
|
||||||
|
reset_label.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 12px;")
|
||||||
|
reset_label.setToolTip("重置本地偏好、域名路由、跳板和当前节点选择;不会删除登录和订阅")
|
||||||
|
reset_row.addWidget(reset_label)
|
||||||
|
reset_row.addStretch()
|
||||||
|
self.btn_reset = QPushButton("重置")
|
||||||
|
self.btn_reset.setCursor(Qt.PointingHandCursor)
|
||||||
|
self.btn_reset.setStyleSheet(option_style)
|
||||||
|
self.btn_reset.setEnabled(not port_locked)
|
||||||
|
self.btn_reset.clicked.connect(self._reset_user_config)
|
||||||
|
reset_row.addWidget(self.btn_reset)
|
||||||
|
layout.addLayout(reset_row)
|
||||||
|
|
||||||
layout.addLayout(log_row)
|
layout.addLayout(log_row)
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
|
|
||||||
@@ -1131,6 +1222,27 @@ class SettingsDialog(QDialog):
|
|||||||
def _open_logs(self):
|
def _open_logs(self):
|
||||||
self._services.open_logs(self)
|
self._services.open_logs(self)
|
||||||
|
|
||||||
|
def _reset_user_config(self):
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
self,
|
||||||
|
"重置用户配置",
|
||||||
|
"确定要恢复本地偏好、域名路由、跳板和当前节点选择的默认值吗?\n\n"
|
||||||
|
"登录凭据、节点缓存、第三方订阅和 PAC 数据不会被删除。",
|
||||||
|
QMessageBox.Yes | QMessageBox.No,
|
||||||
|
QMessageBox.No,
|
||||||
|
)
|
||||||
|
if reply != QMessageBox.Yes:
|
||||||
|
return
|
||||||
|
self._services.reset_user_config()
|
||||||
|
self.ed_port.setText(str(self._services.get_port()))
|
||||||
|
self.cb_builtin_pac.setChecked(self._services.get_builtin_pac())
|
||||||
|
self.cb_update_proxy.setChecked(self._services.get_update_via_proxy())
|
||||||
|
self.cb_skip_update.setChecked(self._services.get_skip_update_check())
|
||||||
|
self.btn_node_domain.setChecked(True)
|
||||||
|
self.btn_close_exit.setChecked(True)
|
||||||
|
self.lb_hint.setText("用户配置已重置")
|
||||||
|
self.lb_hint.setStyleSheet(f"color: {C_GREEN}; font-size: 11px;")
|
||||||
|
|
||||||
def _save_settings(self):
|
def _save_settings(self):
|
||||||
port = self._services.parse_port(self.ed_port.text())
|
port = self._services.parse_port(self.ed_port.text())
|
||||||
if port is None:
|
if port is None:
|
||||||
@@ -1138,8 +1250,15 @@ class SettingsDialog(QDialog):
|
|||||||
self.lb_hint.setStyleSheet(f"color: {C_RED}; font-size: 11px;")
|
self.lb_hint.setStyleSheet(f"color: {C_RED}; font-size: 11px;")
|
||||||
return
|
return
|
||||||
self._services.set_port(port)
|
self._services.set_port(port)
|
||||||
|
self._services.set_builtin_pac(bool(self.cb_builtin_pac.isChecked()))
|
||||||
self._services.set_update_via_proxy(bool(self.cb_update_proxy.isChecked()))
|
self._services.set_update_via_proxy(bool(self.cb_update_proxy.isChecked()))
|
||||||
self._services.set_skip_update_check(bool(self.cb_skip_update.isChecked()))
|
self._services.set_skip_update_check(bool(self.cb_skip_update.isChecked()))
|
||||||
|
self._services.set_node_display_mode(
|
||||||
|
"remark" if self.btn_node_remark.isChecked() else "domain"
|
||||||
|
)
|
||||||
|
self._services.set_close_behavior(
|
||||||
|
"tray" if self.btn_close_tray.isChecked() else "exit"
|
||||||
|
)
|
||||||
self.ed_port.setText(str(port))
|
self.ed_port.setText(str(port))
|
||||||
self.lb_hint.setText("已保存,下次开启代理生效")
|
self.lb_hint.setText("已保存,下次开启代理生效")
|
||||||
self.lb_hint.setStyleSheet(f"color: {C_GREEN}; font-size: 11px;")
|
self.lb_hint.setStyleSheet(f"color: {C_GREEN}; font-size: 11px;")
|
||||||
|
|||||||
Reference in New Issue
Block a user