feat(lite): add tray settings and reset controls
This commit is contained in:
+133
-4
@@ -7,7 +7,7 @@ from pathlib import Path
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication, QWidget, QLabel, QPushButton,
|
||||
QVBoxLayout, QHBoxLayout, QFrame, QMessageBox, QTextEdit,
|
||||
QCheckBox, QMenu, QDialog, QProgressDialog,
|
||||
QCheckBox, QMenu, QDialog, QProgressDialog, QAction, QSystemTrayIcon,
|
||||
)
|
||||
from PyQt5.QtCore import Qt, QTimer, pyqtSignal
|
||||
from PyQt5.QtGui import QFont, QIcon, QPixmap, QImage, QColor
|
||||
@@ -816,7 +816,9 @@ def _endpoint_label():
|
||||
domain = _endpoint_domain()
|
||||
if domain == "???":
|
||||
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):
|
||||
"""正向代理 Basic 认证凭证:methods.http.{username,password}(新格式)
|
||||
@@ -1433,6 +1435,47 @@ def _set_bypass_lan(enabled):
|
||||
data["bypass_lan"] = bool(enabled)
|
||||
_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 键) ────────
|
||||
# 每条规则 {"value": 域名/IP/CIDR, "enabled": 是否启用,
|
||||
# "action": "direct" | "node", "node_ref": 节点稳定标识}。
|
||||
@@ -2633,11 +2676,16 @@ def _settings_services():
|
||||
get_builtin_pac=_use_builtin_pac,
|
||||
get_update_via_proxy=_update_via_proxy,
|
||||
get_skip_update_check=_skip_update_check,
|
||||
get_node_display_mode=_node_display_mode,
|
||||
get_close_behavior=_close_behavior,
|
||||
parse_port=_parse_port,
|
||||
set_port=_set_local_port,
|
||||
set_builtin_pac=_set_builtin_pac,
|
||||
set_update_via_proxy=_set_update_via_proxy,
|
||||
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_jump=lambda parent: JumpDialog(_jump_services(), parent).exec_(),
|
||||
open_third_party=lambda parent: ThirdPartyDialog(_third_party_services(), parent).exec_(),
|
||||
@@ -2674,6 +2722,9 @@ class MainWindow(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._proxy_set_by_us = False # 系统代理是否由本程序开启(退出时只清理自己设置的)
|
||||
self._quitting = False
|
||||
self._tray = None
|
||||
self._tray_menu = None
|
||||
# 启动完成前仍允许用户取消。事件由 GUI 线程置位,后台启动线程只读取;
|
||||
# 锁保护系统代理写入和取消清理之间的交接,避免遗留代理。
|
||||
self._start_cancel_event = None
|
||||
@@ -2695,6 +2746,7 @@ class MainWindow(QWidget):
|
||||
_user_init_local() # 首启初始化用户数据目录(节点模板),须在 init_ui 读节点之前
|
||||
|
||||
self.init_ui()
|
||||
self._setup_tray()
|
||||
|
||||
# 每 2 秒刷新一次状态指示灯
|
||||
self.timer = QTimer()
|
||||
@@ -2714,6 +2766,65 @@ class MainWindow(QWidget):
|
||||
# 节点未配置时延迟弹出登录框(等窗口显示后)
|
||||
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):
|
||||
"""等节点配置在线同步完成后,校验当前选中节点是否可用,必要时自动回退。
|
||||
|
||||
@@ -2904,7 +3015,11 @@ class MainWindow(QWidget):
|
||||
layout.addWidget(hints)
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""关闭窗口时自动清理代理和隧道。"""
|
||||
"""按用户设置退出或隐藏到托盘。"""
|
||||
if not self._quitting and _close_behavior() == "tray" and self._tray is not None:
|
||||
event.ignore()
|
||||
self.hide()
|
||||
return
|
||||
try:
|
||||
self.quit_app()
|
||||
finally:
|
||||
@@ -3114,8 +3229,15 @@ class MainWindow(QWidget):
|
||||
def _show_settings_dialog(self):
|
||||
"""打开设置弹窗;第三方订阅与登录状态可独立管理。"""
|
||||
SettingsDialog(_settings_services(), self, port_locked=self._proxy_set_by_us).exec_()
|
||||
self._refresh_preferences()
|
||||
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):
|
||||
"""首次无节点时提示登录;用户可跳过并在主界面添加第三方订阅。"""
|
||||
@@ -3206,6 +3328,7 @@ class MainWindow(QWidget):
|
||||
def _refresh_subtitle(self):
|
||||
"""刷新节点副标题文字。"""
|
||||
self.subtitle.setText(_endpoint_label())
|
||||
self._update_tray_menu()
|
||||
|
||||
def _update_node_label(self):
|
||||
"""代理运行中时提示禁止切换节点。"""
|
||||
@@ -3299,6 +3422,7 @@ class MainWindow(QWidget):
|
||||
self.traffic_up.setText(f"↑{_fmt_bytes(up_rate)}/s")
|
||||
self.traffic_down.setText(f"↓{_fmt_bytes(down_rate)}/s")
|
||||
self.traffic_total.setText(_fmt_bytes(up_total + down_total))
|
||||
self._update_tray_menu()
|
||||
def _check_update(self, manual=False):
|
||||
"""后台检查服务器是否有新版本;manual=True 时无视「跳过检查更新」设置。"""
|
||||
# 先初始化结果,任何路径(含跳过)完成后 _on_update_done 都能安全读取
|
||||
@@ -3515,6 +3639,9 @@ class MainWindow(QWidget):
|
||||
|
||||
本程序异常退出残留的代理设置由下次启动时 _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:
|
||||
self._start_cancel_event.set()
|
||||
try:
|
||||
@@ -3608,7 +3735,9 @@ def main():
|
||||
app = QApplication(sys.argv)
|
||||
icon = load_icon()
|
||||
app.setWindowIcon(icon)
|
||||
app.setQuitOnLastWindowClosed(True)
|
||||
# 主窗口可按设置隐藏到托盘;显式点击托盘“退出”或关闭行为为“退出”时,
|
||||
# MainWindow.quit_app() 会负责结束 QApplication。
|
||||
app.setQuitOnLastWindowClosed(False)
|
||||
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
|
||||
Reference in New Issue
Block a user