Compare commits

...
Author SHA1 Message Date
RoyirinRosmontis 2120009c8c 修复:
- 手动点击「检查更新」无视「跳过检查更新」设置:必定输出日志,发现新版本时照常弹窗询问下载(跳过设置只作用于启动时的自动检查)
2026-08-24 12:35:41 +08:00
RoyirinRosmontis 5620f0051e 修复:
- 版本号与"检查更新"间距调整为 4px
2026-08-24 12:32:30 +08:00
RoyirinRosmontis f8e8fd1acd 修复:
- 版本号与"检查更新"间距从 16px 缩到 6px(约一两个空格)
2026-08-24 12:31:35 +08:00
RoyirinRosmontis d1369d02d4 修复:
- 设置弹窗版本号与「检查更新」入口改为整体居中,中间隔 16px 小空
2026-08-24 12:30:10 +08:00
RoyirinRosmontis ebccfb4165 新增功能:
- 设置弹窗版本号左对齐,右侧新增「检查更新」入口(节点同款 hover),点击手动触发一次检查更新(后台执行,完成后提示见主界面日志)
2026-08-24 12:28:59 +08:00
RoyirinRosmontis b9a35684ac 新增功能:
- 设置弹窗标题下固定显示当前版本号(config.json 的 version)
2026-08-24 12:27:10 +08:00
RoyirinRosmontis 7e6c95f78a 修复:
- 保存更改成功提示去掉端口号显示,统一为"已保存,下次开启代理生效"
2026-08-24 12:24:31 +08:00
RoyirinRosmontis 54385d72ce 修复:
- ToggleSwitch 开关点击无反应:自绘 QCheckBox 在部分样式引擎下默认点击不触发切换,改为显式处理鼠标按下事件切换并重绘
2026-08-24 12:21:00 +08:00
RoyirinRosmontis 2652213a10 新增功能:
- 设置页新增「跳过检查更新」开关(settings.json 的 skip_update_check,默认关):开启后跳过 PAC 最新版拉取与程序版本检查且不输出相关日志;节点配置更新与客户端认证不受影响
2026-08-24 12:16:55 +08:00
RoyirinRosmontis 4afb810078 修复:
- 设置弹窗提示语移到"设置"标题正下方(保存结果/错误信息同位置显示)
2026-08-24 12:12:26 +08:00
RoyirinRosmontis 3519fbe9e3 修复:
- 设置页「使用代理更新」改为左文字 + 右侧苹果风格开关(ToggleSwitch 自绘:绿/灰胶囊+滑动圆钮,无图片资源)
2026-08-24 12:10:28 +08:00
RoyirinRosmontis a08ca154c9 新增功能:
- 设置页新增「使用代理更新」开关(用户级 settings.json 的 update_via_proxy,默认关):开启后检查更新与下载走节点隧道(CONNECT),关闭时直连仓库 releases API——直连不通的网络可开启隧道,默认直连避免节点隧道故障影响更新检查
2026-08-24 12:07:42 +08:00
RoyirinRosmontis f92c3f52e0 修复:
- 更新检查超时从 10 秒放宽到 20 秒(隧道到 Gitea 链路较慢,避免慢网被误判为失败)
2026-08-24 11:48:26 +08:00
RoyirinRosmontis 22498cb884 修复:
- 移除设置弹窗标题下方的分割线(视觉效果不佳)
2026-08-23 15:24:43 +08:00
RoyirinRosmontis 8e550cba6b 修复:
- 设置弹窗标题"设置"下方增加分割线(与主界面分隔线同款)
2026-08-23 15:08:37 +08:00
+165 -37
View File
@@ -8,8 +8,8 @@ from PyQt5.QtWidgets import (
QVBoxLayout, QHBoxLayout, QFrame, QMessageBox, QTextEdit, QVBoxLayout, QHBoxLayout, QFrame, QMessageBox, QTextEdit,
QCheckBox, QMenu, QDialog, QLineEdit, QCheckBox, QMenu, QDialog, QLineEdit,
) )
from PyQt5.QtCore import Qt, QTimer, pyqtSignal from PyQt5.QtCore import Qt, QTimer, QRectF, pyqtSignal
from PyQt5.QtGui import QFont, QIcon, QPixmap, QImage, QColor from PyQt5.QtGui import QFont, QIcon, QPixmap, QImage, QColor, QPainter
# GUI 窗口在 96 DPI / 100% 缩放下的逻辑尺寸;启用 Qt 高分屏缩放后, # GUI 窗口在 96 DPI / 100% 缩放下的逻辑尺寸;启用 Qt 高分屏缩放后,
# Qt 会自动按 devicePixelRatio 放大/缩小,因此代码里保持逻辑尺寸不变即可。 # Qt 会自动按 devicePixelRatio 放大/缩小,因此代码里保持逻辑尺寸不变即可。
@@ -590,6 +590,29 @@ def _set_local_port(port):
_settings_save(data) _settings_save(data)
log(f"代理转发端口已保存: {port}(下次开启代理生效)") log(f"代理转发端口已保存: {port}(下次开启代理生效)")
def _update_via_proxy():
"""更新检查/下载是否走节点隧道(CONNECT);设置项,默认关=直连仓库。"""
return bool(_settings_load().get("update_via_proxy", False))
def _set_update_via_proxy(enabled):
"""写回「使用代理更新」设置(立即生效)。"""
data = _settings_load()
data["update_via_proxy"] = bool(enabled)
_settings_save(data)
log(f"更新方式已设为: {'走代理隧道' if enabled else '直连仓库'}")
def _skip_update_check():
"""是否跳过 PAC 数据与程序版本检查(设置项,默认关)。
不影响节点配置更新与客户端认证。"""
return bool(_settings_load().get("skip_update_check", False))
def _set_skip_update_check(enabled):
"""写回「跳过检查更新」设置(立即生效)。"""
data = _settings_load()
data["skip_update_check"] = bool(enabled)
_settings_save(data)
log(f"检查更新已{'跳过' if enabled else '恢复'}(PAC 与程序版本;节点更新不受影响)")
def _ver_tuple(v): def _ver_tuple(v):
"""版本号转数字元组:'v0.1.0' -> (0, 1, 0),无法解析的段按 0 处理。""" """版本号转数字元组:'v0.1.0' -> (0, 1, 0),无法解析的段按 0 处理。"""
parts = [] parts = []
@@ -810,6 +833,8 @@ def _user_init_remote(base_url=None):
直接拉取两个数据文件(内含 version/date),按版本比对后更新;失败用本地副本。 直接拉取两个数据文件(内含 version/date),按版本比对后更新;失败用本地副本。
节点配置的拉取走登录框(nodes_url 需 nginx 认证,见 LoginDialog)。 节点配置的拉取走登录框(nodes_url 需 nginx 认证,见 LoginDialog)。
""" """
if _skip_update_check():
return # 设置跳过:不拉取也不打日志
base = (base_url or str(_config_val("pac_base_url", "") or "")).strip().rstrip("/") base = (base_url or str(_config_val("pac_base_url", "") or "")).strip().rstrip("/")
if not base: if not base:
log("未配置 PAC 更新源(config.json 的 pac_base_url),使用本地数据") log("未配置 PAC 更新源(config.json 的 pac_base_url),使用本地数据")
@@ -1390,7 +1415,7 @@ class SettingsDialog(QDialog):
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("设置") self.setWindowTitle("设置")
self.setWindowIcon(load_icon()) self.setWindowIcon(load_icon())
self.setFixedSize(300, 230) self.setFixedSize(300, 316)
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; }}
@@ -1413,6 +1438,29 @@ class SettingsDialog(QDialog):
title.setAlignment(Qt.AlignCenter) title.setAlignment(Qt.AlignCenter)
layout.addWidget(title) layout.addWidget(title)
# 版本号 + 手动「检查更新」入口(整体居中,中间隔小空)
ver_row = QHBoxLayout()
ver_row.addStretch()
ver_lab = QLabel(f"版本号: {VERSION}")
ver_lab.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 11px;")
ver_row.addWidget(ver_lab)
ver_row.addSpacing(4)
self.link_check_update = NodeLabel(
"检查更新", tooltip="手动检查更新",
base=f"color: {C_SUBTEXT}; font-size: 11px;",
hover=f"color: {C_TEXT}; font-size: 11px; text-decoration: underline;")
self.link_check_update.clicked.connect(self._manual_check_update)
ver_row.addWidget(self.link_check_update)
ver_row.addStretch()
layout.addLayout(ver_row)
# 提示语:标题正下方(保存结果/错误也显示在这行)
self.lb_hint = QLabel("代理运行中,关闭代理后可修改" if port_locked else "保存更改后下次开启代理生效")
self.lb_hint.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 11px;")
self.lb_hint.setFixedHeight(14)
self.lb_hint.setAlignment(Qt.AlignCenter)
layout.addWidget(self.lb_hint)
layout.addStretch() # 设置项在空白部分纵向居中(上下均分) layout.addStretch() # 设置项在空白部分纵向居中(上下均分)
# 代理转发端口 # 代理转发端口
@@ -1427,14 +1475,29 @@ class SettingsDialog(QDialog):
port_row.addWidget(self.ed_port) port_row.addWidget(self.ed_port)
layout.addLayout(port_row) layout.addLayout(port_row)
layout.addStretch() # 更新方式:左文字 + 右苹果式开关(默认关 = 直连仓库)
upd_row = QHBoxLayout()
upd_lab = QLabel("使用代理更新")
upd_lab.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 12px;")
upd_row.addWidget(upd_lab)
upd_row.addStretch()
self.cb_update_proxy = ToggleSwitch(checked=_update_via_proxy())
self.cb_update_proxy.setToolTip("开启后检查更新与下载走节点隧道(CONNECT);默认直连仓库")
upd_row.addWidget(self.cb_update_proxy)
layout.addLayout(upd_row)
# 提示语:按钮上方居中 # 跳过检查更新:左文字 + 右开关(默认关;不影响节点更新与认证)
self.lb_hint = QLabel("代理运行中,关闭代理后可修改" if port_locked else "保存更改后下次开启代理生效") skip_row = QHBoxLayout()
self.lb_hint.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 11px;") skip_lab = QLabel("跳过检查更新")
self.lb_hint.setFixedHeight(14) skip_lab.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 12px;")
self.lb_hint.setAlignment(Qt.AlignCenter) skip_row.addWidget(skip_lab)
layout.addWidget(self.lb_hint) skip_row.addStretch()
self.cb_skip_update = ToggleSwitch(checked=_skip_update_check())
self.cb_skip_update.setToolTip("跳过 PAC 数据与程序版本检查(节点更新与登录不受影响),被跳过的检查不输出日志")
skip_row.addWidget(self.cb_skip_update)
layout.addLayout(skip_row)
layout.addStretch()
# 底部:保存更改 + 退出登录 并排 # 底部:保存更改 + 退出登录 并排
btns = QHBoxLayout() btns = QHBoxLayout()
@@ -1467,6 +1530,27 @@ class SettingsDialog(QDialog):
btns.addStretch() btns.addStretch()
layout.addLayout(btns) layout.addLayout(btns)
def _manual_check_update(self):
"""手动触发一次检查更新(后台线程,复用主窗口逻辑)。"""
parent = self.parent()
if not isinstance(parent, MainWindow):
return
self.lb_hint.setText("正在检查更新...")
self.lb_hint.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 11px;")
try:
parent._update_done.connect(self._on_check_done)
except TypeError:
pass # 已连接过,忽略
threading.Thread(target=parent._check_update, kwargs={"manual": True}, daemon=True).start()
def _on_check_done(self):
self.lb_hint.setText("检查完成,结果见主界面日志")
self.lb_hint.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 11px;")
try:
self.parent()._update_done.disconnect(self._on_check_done)
except Exception:
pass
def _save_settings(self): def _save_settings(self):
"""保存更改:统一校验并应用全部设置项(后续新增设置项在此扩展)。""" """保存更改:统一校验并应用全部设置项(后续新增设置项在此扩展)。"""
port = _parse_port(self.ed_port.text()) port = _parse_port(self.ed_port.text())
@@ -1475,8 +1559,10 @@ 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
_set_local_port(port) _set_local_port(port)
_set_update_via_proxy(bool(self.cb_update_proxy.isChecked()))
_set_skip_update_check(bool(self.cb_skip_update.isChecked()))
self.ed_port.setText(str(port)) self.ed_port.setText(str(port))
self.lb_hint.setText(f"已保存: {port},下次开启代理生效") 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;")
def _logout(self): def _logout(self):
@@ -1491,6 +1577,39 @@ class SettingsDialog(QDialog):
log("已退出登录:凭据与节点配置已清除,下次启动或开启代理时需重新登录") log("已退出登录:凭据与节点配置已清除,下次启动或开启代理时需重新登录")
self.accept() self.accept()
# ── 苹果风格开关 ─────────────────────────────────────
class ToggleSwitch(QCheckBox):
"""苹果风格开关:左关右开,绿/灰胶囊 + 滑动圆钮(自绘,无图片资源)。"""
def __init__(self, checked=False, parent=None):
super().__init__(parent)
self.setChecked(checked)
self.setFixedSize(42, 22)
self.setCursor(Qt.PointingHandCursor)
def paintEvent(self, e):
p = QPainter(self)
p.setRenderHint(QPainter.Antialiasing)
p.setPen(Qt.NoPen)
# 胶囊背景:开=主题绿,关=浅灰
p.setBrush(QColor(C_GREEN) if self.isChecked() else QColor("#c8ccd4"))
p.drawRoundedRect(QRectF(0, 0, 42, 22), 11, 11)
# 白色圆钮:开在右,关在左
knob_x = 20.0 if self.isChecked() else 2.0
p.setBrush(QColor("#ffffff"))
p.drawEllipse(QRectF(knob_x, 2.0, 18.0, 18.0))
p.end()
def mousePressEvent(self, e):
# 显式处理点击切换:自绘控件在部分样式引擎下默认点击不触发
if e.button() == Qt.LeftButton:
self.setChecked(not self.isChecked())
self.repaint()
e.accept()
return
super().mousePressEvent(e)
# ── 状态指示灯(小圆点) ─────────────────────────────── # ── 状态指示灯(小圆点) ───────────────────────────────
class StatusDot(QLabel): class StatusDot(QLabel):
@@ -2026,35 +2145,37 @@ 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))
def _check_update(self): def _check_update(self, manual=False):
"""后台检查服务器是否有新版本。""" """后台检查服务器是否有新版本;manual=True 时无视「跳过检查更新」设置。"""
# 先初始化结果,任何路径(含跳过)完成后 _on_update_done 都能安全读取 # 先初始化结果,任何路径(含跳过)完成后 _on_update_done 都能安全读取
self._latest_version = "" self._latest_version = ""
self._download_url = "" self._download_url = ""
try: try:
import json, urllib.request, base64 if _skip_update_check() and not manual:
domain = _endpoint_domain() return # 设置跳过(仅自动检查):不检查也不打日志
if domain == "???": import json, urllib.request
log("更新检查: 未配置节点(nodes.json 域名为空),跳过")
return
# 基于 Gitea 仓库 releases API 检查更新;config.json 无 update_url 时用默认地址 # 基于 Gitea 仓库 releases API 检查更新;config.json 无 update_url 时用默认地址
url = str(_config_val("update_url", "") or "").strip() url = str(_config_val("update_url", "") or "").strip()
if not url: if not url:
url = ("https://git.bu4.ren/api/v1/repos/RoyirinRosmontis/" url = ("https://git.bu4.ren/api/v1/repos/RoyirinRosmontis/"
"Private-Proxy/releases/latest") "Private-Proxy/releases/latest")
log("正在检查更新...") log("正在检查更新...")
# 显式走本地转发器隧道,不依赖系统代理状态:默认 opener 读注册表代理设置 if _update_via_proxy():
# (异常退出残留 127.0.0.1 代理 -> 连接被拒),不识别 PAC 的 AutoConfigURL, # 走节点隧道(CONNECT,设置「使用代理更新」开启时);显式代理避免残留干扰
# 直连服务器还可能被墙干扰 TLS 握手(SSL UNEXPECTED_EOF) started_here = _ensure_forwarder()
started_here = _ensure_forwarder() try:
try: opener = urllib.request.build_opener(urllib.request.ProxyHandler(
opener = urllib.request.build_opener(urllib.request.ProxyHandler( {"http": f"{PROXY_HOST}:{PROXY_PORT}",
{"http": f"{PROXY_HOST}:{PROXY_PORT}", "https": f"{PROXY_HOST}:{PROXY_PORT}"}))
"https": f"{PROXY_HOST}:{PROXY_PORT}"})) req = urllib.request.Request(url)
data = opener.open(req, timeout=20).read().decode("utf-8-sig")
finally:
_release_forwarder(started_here)
else:
# 默认直连仓库;显式空代理避免系统/环境残留代理干扰
opener = _pac_no_proxy_opener()
req = urllib.request.Request(url) req = urllib.request.Request(url)
data = opener.open(req, timeout=10).read().decode("utf-8-sig") data = opener.open(req, timeout=20).read().decode("utf-8-sig")
finally:
_release_forwarder(started_here)
info = json.loads(data) info = json.loads(data)
# 基于 Gitea 仓库 releases API:版本取 tag_name,下载取 exe 附件链接 # 基于 Gitea 仓库 releases API:版本取 tag_name,下载取 exe 附件链接
self._latest_version = str(info.get("tag_name") or "") self._latest_version = str(info.get("tag_name") or "")
@@ -2097,18 +2218,25 @@ class MainWindow(QWidget):
log(f"正在下载新版本: {url}") log(f"正在下载新版本: {url}")
log(f"保存到: {save_path}") log(f"保存到: {save_path}")
# 同更新检查:显式走本地转发器隧道,不依赖系统代理状态 # 同更新检查:按「使用代理更新」设置选择隧道或直连
started_here = _ensure_forwarder() if _update_via_proxy():
try: started_here = _ensure_forwarder()
opener = urllib.request.build_opener(urllib.request.ProxyHandler( try:
{"http": f"{PROXY_HOST}:{PROXY_PORT}", opener = urllib.request.build_opener(urllib.request.ProxyHandler(
"https": f"{PROXY_HOST}:{PROXY_PORT}"})) {"http": f"{PROXY_HOST}:{PROXY_PORT}",
"https": f"{PROXY_HOST}:{PROXY_PORT}"}))
req = urllib.request.Request(url)
with opener.open(req, timeout=120) as resp:
data = resp.read()
finally:
_release_forwarder(started_here)
else:
opener = _pac_no_proxy_opener()
req = urllib.request.Request(url) req = urllib.request.Request(url)
with opener.open(req, timeout=120) as resp: with opener.open(req, timeout=120) as resp:
data = resp.read() data = resp.read()
finally:
_release_forwarder(started_here)
if len(data) < 1024: if len(data) < 1024:
self._update_download_done.emit(False, "下载文件过小,可能不是有效的 EXE") self._update_download_done.emit(False, "下载文件过小,可能不是有效的 EXE")