Files
Private-Proxy/lite/rrpp_ui/dialogs.py
T

663 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""需要业务服务的 Qt 对话框。"""
import json
import threading
from dataclasses import dataclass
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem,
QPushButton, QVBoxLayout, QWidget,
)
from .theme import C_BG, C_BORDER, C_CARD, C_GREEN, C_RED, C_SUBTEXT, C_TEXT, C_TITLE
from .widgets import NodeLabel, ToggleSwitch
@dataclass(frozen=True)
class LoginServices:
"""登录框所需的显式业务依赖,避免 UI 反向导入程序入口。"""
load_icon: object
load_auth: object
get_nodes_url: object
fetch_nodes: object
user_dir: object
encrypt_nodes: object
save_auth: object
auth_error: type
log: object
class LoginDialog(QDialog):
"""使用 SSO 获取并加密保存节点配置的登录框。"""
_done = pyqtSignal(bool, str)
def __init__(self, services: LoginServices, parent=None):
super().__init__(parent)
self._services = services
self.setWindowTitle("节点配置登录")
self.setWindowIcon(services.load_icon())
self.setFixedSize(420, 170)
self.setStyleSheet(f"""
QDialog {{ background-color: {C_BG}; }}
QLabel {{ color: {C_TEXT}; font-family: "Microsoft YaHei", "Segoe UI", sans-serif; font-size: 12px; }}
QLineEdit {{
background-color: {C_CARD}; border: 1px solid {C_BORDER}; border-radius: 6px;
padding: 6px 8px; color: {C_TEXT}; font-size: 12px;
}}
QPushButton#btnOk {{
background-color: {C_GREEN}; color: #fff; border: none; border-radius: 6px;
padding: 4px 12px; font-size: 12px; font-weight: bold;
}}
QPushButton#btnOk:hover {{ background-color: #5ed978; }}
QPushButton#btnOk:disabled {{ background-color: #2a3a2e; color: #666; }}
QPushButton#btnCancel {{
background-color: {C_CARD}; color: {C_SUBTEXT}; border: 1px solid {C_BORDER};
border-radius: 6px; padding: 4px 12px; font-size: 12px;
}}
""")
root = QHBoxLayout(self)
root.setSpacing(14)
root.setContentsMargins(20, 18, 20, 16)
left = QVBoxLayout()
left.setSpacing(6)
left.addStretch()
logo = QLabel()
logo.setPixmap(services.load_icon().pixmap(80, 80))
logo.setFixedSize(80, 80)
logo.setAlignment(Qt.AlignCenter)
left.addWidget(logo, 0, Qt.AlignHCenter)
brand = QLabel("R.R.P.P.")
brand.setStyleSheet(f"color: {C_TITLE}; font-size: 10px;")
brand.setAlignment(Qt.AlignCenter)
left.addWidget(brand, 0, Qt.AlignHCenter)
left.addStretch()
root.addLayout(left)
form = QVBoxLayout()
form.setSpacing(6)
self.ed_user = QLineEdit()
self.ed_user.setPlaceholderText("SSO 邮箱")
form.addWidget(self.ed_user)
self.ed_pass = QLineEdit()
self.ed_pass.setPlaceholderText("密码")
self.ed_pass.setEchoMode(QLineEdit.Password)
self.ed_pass.returnPressed.connect(self._submit)
form.addWidget(self.ed_pass)
credentials = services.load_auth()
if credentials:
self.ed_user.setText(credentials["username"])
self.ed_pass.setText(credentials["password"])
self.lb_err = QLabel("")
self.lb_err.setStyleSheet(f"color: {C_RED}; font-size: 11px;")
self.lb_err.setFixedHeight(14)
self.lb_err.hide()
form.addWidget(self.lb_err)
bottom = QHBoxLayout()
bottom.setSpacing(10)
hint = QLabel("登录以获取节点配置")
hint.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 10px;")
bottom.addWidget(hint)
bottom.addStretch()
self.btn_cancel = QPushButton("跳过登录")
self.btn_cancel.setObjectName("btnCancel")
self.btn_cancel.setToolTip("可稍后在设置中登录,或添加第三方订阅")
self.btn_cancel.clicked.connect(self.reject)
bottom.addWidget(self.btn_cancel)
self.btn_ok = QPushButton("登录")
self.btn_ok.setObjectName("btnOk")
self.btn_ok.clicked.connect(self._submit)
bottom.addWidget(self.btn_ok)
form.addLayout(bottom)
box = QWidget()
box.setLayout(form)
root.addWidget(box, 1, Qt.AlignVCenter)
self._done.connect(self._on_done)
def _submit(self):
user = self.ed_user.text().strip()
password = self.ed_pass.text()
if not user or not password:
self.lb_err.setText("请输入用户名和密码")
self.lb_err.show()
return
self.btn_ok.setEnabled(False)
self.btn_cancel.setEnabled(False)
self.btn_ok.setText("登录中")
threading.Thread(target=self._worker, args=(user, password), daemon=True).start()
def _worker(self, user, password):
try:
url = str(self._services.get_nodes_url() or "").strip()
if not url:
raise RuntimeError("config.json 未配置 nodes_url")
data = self._services.fetch_nodes(url, user, password)
if not (isinstance(data, list) and data and all(isinstance(node, dict) for node in data)
and str(data[0].get("domain") or "").strip()):
raise ValueError("返回的节点数据无效")
text = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
directory = self._services.user_dir()
directory.mkdir(parents=True, exist_ok=True)
(directory / "nodes.json").write_bytes(self._services.encrypt_nodes(text))
self._services.save_auth(user, password)
self._done.emit(True, str(len(data)))
except self._services.auth_error as error:
self._done.emit(False, str(error))
except Exception as error:
self._done.emit(False, f"登录失败: {error}")
def _on_done(self, ok, info):
self.btn_ok.setEnabled(True)
self.btn_cancel.setEnabled(True)
self.btn_ok.setText("登录")
if ok:
self._services.log(f"登录成功,已获取 {info} 个节点(nodes.json 已加密保存)")
self.accept()
else:
self.lb_err.setText(info)
self.lb_err.show()
@dataclass(frozen=True)
class WhitelistServices:
"""白名单编辑框所需的规则服务。"""
load_icon: object
load_rules: object
validate_rule: object
save_rules: object
class WhitelistDialog(QDialog):
"""编辑强制直连白名单;规则数据通过显式服务读写。"""
def __init__(self, services: WhitelistServices, parent=None):
super().__init__(parent)
self._services = services
self.setWindowTitle("代理白名单")
self.setWindowIcon(services.load_icon())
self.setFixedSize(380, 400)
self.setStyleSheet(f"""
QDialog {{ background-color: {C_BG}; }}
QLabel {{ color: {C_TEXT}; font-family: "Microsoft YaHei", "Segoe UI", sans-serif; font-size: 12px; }}
QLineEdit {{ background-color: {C_CARD}; border: 1px solid {C_BORDER}; border-radius: 6px;
padding: 6px 8px; color: {C_TEXT}; font-size: 12px; }}
QListWidget {{ background-color: {C_CARD}; border: 1px solid {C_BORDER}; border-radius: 6px;
padding: 4px; color: {C_TEXT}; font-size: 12px; }}
QListWidget::item {{ padding: 3px 6px; border-radius: 4px; }}
QListWidget::item:selected {{ background-color: #e3e8ee; color: {C_TEXT}; }}
QPushButton#btnOk {{ background-color: {C_GREEN}; color: #fff; border: none;
border-radius: 6px; padding: 4px 12px; font-size: 12px; font-weight: bold; }}
QPushButton#btnOk:hover {{ background-color: #5ed978; }}
QPushButton#btnCancel {{ background-color: {C_CARD}; color: {C_SUBTEXT}; border: 1px solid {C_BORDER};
border-radius: 6px; padding: 4px 12px; font-size: 12px; }}
QPushButton#btnSmall {{ background-color: {C_CARD}; color: {C_SUBTEXT}; border: 1px solid {C_BORDER};
border-radius: 6px; padding: 3px 10px; font-size: 11px; }}
QPushButton#btnSmall:hover {{ background-color: #e3e8ee; color: {C_TEXT}; }}
""")
layout = QVBoxLayout(self)
layout.setSpacing(10)
layout.setContentsMargins(20, 16, 20, 16)
title = QLabel("代理白名单")
title.setStyleSheet(f"color: {C_TITLE}; font-size: 15px; font-weight: bold;")
title.setAlignment(Qt.AlignCenter)
layout.addWidget(title)
hint = QLabel("命中以下规则的连接将强制直连(不经过代理)")
hint.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 11px;")
hint.setAlignment(Qt.AlignCenter)
layout.addWidget(hint)
self.lb_err = QLabel("")
self.lb_err.setStyleSheet(f"color: {C_RED}; font-size: 11px;")
self.lb_err.setFixedHeight(14)
self.lb_err.hide()
layout.addWidget(self.lb_err)
self.list_widget = QListWidget()
self.list_widget.setToolTip("勾选启用规则;可多选后删除")
for rule in services.load_rules():
self._add_item(rule["value"], rule.get("enabled", True))
layout.addWidget(self.list_widget, 1)
add_row = QHBoxLayout()
add_row.setSpacing(8)
self.ed_add = QLineEdit()
self.ed_add.setPlaceholderText("输入域名 / IP / CIDR,如 example.com")
self.ed_add.returnPressed.connect(self._add_rule)
add_row.addWidget(self.ed_add, 1)
self.btn_add = QPushButton("添加")
self.btn_add.setObjectName("btnOk")
self.btn_add.setStyleSheet(f"""
QPushButton {{ background-color: {C_GREEN}; color: #fff; border: none; border-radius: 6px;
padding: 6px 14px; font-size: 12px; font-weight: bold; }}
QPushButton:hover {{ background-color: #5ed978; }}
""")
self.btn_add.clicked.connect(self._add_rule)
add_row.addWidget(self.btn_add)
layout.addLayout(add_row)
middle = QHBoxLayout()
self.btn_delete = QPushButton("删除选中")
self.btn_delete.setObjectName("btnSmall")
self.btn_delete.clicked.connect(self._delete_selected)
middle.addWidget(self.btn_delete)
middle.addStretch()
layout.addLayout(middle)
buttons = QHBoxLayout()
buttons.setSpacing(10)
buttons.addStretch()
self.btn_cancel = QPushButton("取消")
self.btn_cancel.setObjectName("btnCancel")
self.btn_cancel.clicked.connect(self.reject)
buttons.addWidget(self.btn_cancel)
self.btn_ok = QPushButton("保存")
self.btn_ok.setObjectName("btnOk")
self.btn_ok.clicked.connect(self._save)
buttons.addWidget(self.btn_ok)
layout.addLayout(buttons)
def _add_item(self, value, enabled):
item = QListWidgetItem(value)
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
item.setCheckState(Qt.Checked if enabled else Qt.Unchecked)
self.list_widget.addItem(item)
def _add_rule(self):
value, error = self._services.validate_rule(self.ed_add.text())
if error:
self.lb_err.setText(error)
self.lb_err.show()
return
for index in range(self.list_widget.count()):
if self.list_widget.item(index).text().lower() == value.lower():
self.lb_err.setText("该规则已存在")
self.lb_err.show()
return
self.lb_err.hide()
self._add_item(value, True)
self.ed_add.clear()
def _delete_selected(self):
for item in self.list_widget.selectedItems():
self.list_widget.takeItem(self.list_widget.row(item))
def _save(self):
rules = []
for index in range(self.list_widget.count()):
item = self.list_widget.item(index)
value = str(item.text()).strip()
if value:
rules.append({"value": value, "enabled": item.checkState() == Qt.Checked})
self._services.save_rules(rules)
self.accept()
@dataclass(frozen=True)
class ThirdPartyServices:
"""第三方订阅管理框所需的显式业务服务。"""
load_icon: object
load_subscriptions: object
add_subscription: object
refresh_subscription: object
remove_subscription: object
class ThirdPartyDialog(QDialog):
"""管理多个第三方 RRPP Client JSON 订阅。"""
_done = pyqtSignal(str, bool, str)
def __init__(self, services: ThirdPartyServices, parent=None):
super().__init__(parent)
self._services = services
self._busy = False
self.setWindowTitle("第三方订阅")
self.setWindowIcon(services.load_icon())
self.setFixedSize(520, 440)
self.setStyleSheet(f"""
QDialog {{ background-color: {C_BG}; }}
QLabel {{ color: {C_TEXT}; font-family: "Microsoft YaHei", "Segoe UI", sans-serif; font-size: 12px; }}
QLineEdit {{ background-color: {C_CARD}; border: 1px solid {C_BORDER}; border-radius: 6px;
padding: 5px 8px; color: {C_TEXT}; font-size: 12px; }}
QListWidget {{ background-color: {C_CARD}; border: 1px solid {C_BORDER}; border-radius: 6px;
padding: 4px; color: {C_TEXT}; font-size: 12px; }}
QListWidget::item {{ padding: 5px 7px; border-radius: 4px; }}
QListWidget::item:selected {{ background-color: #e3e8ee; color: {C_TEXT}; }}
QPushButton#btnOk {{ background-color: {C_GREEN}; color: #fff; border: none;
border-radius: 6px; padding: 5px 12px; font-size: 12px; font-weight: bold; }}
QPushButton#btnOk:hover {{ background-color: #5ed978; }}
QPushButton#btnDanger {{ background-color: {C_RED}; color: #fff; border: none;
border-radius: 6px; padding: 5px 12px; font-size: 12px; font-weight: bold; }}
QPushButton#btnSmall {{ background-color: {C_CARD}; color: {C_SUBTEXT}; border: 1px solid {C_BORDER};
border-radius: 6px; padding: 4px 10px; font-size: 11px; }}
QPushButton#btnSmall:hover {{ background-color: #e3e8ee; color: {C_TEXT}; }}
""")
layout = QVBoxLayout(self)
layout.setSpacing(9)
layout.setContentsMargins(20, 16, 20, 16)
title = QLabel("第三方订阅")
title.setStyleSheet(f"color: {C_TITLE}; font-size: 15px; font-weight: bold;")
title.setAlignment(Qt.AlignCenter)
layout.addWidget(title)
hint = QLabel("仅接受 RRPP Client 的 JSON 节点数组;订阅地址和节点凭据会加密保存在本机")
hint.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 10px;")
hint.setAlignment(Qt.AlignCenter)
layout.addWidget(hint)
self.list_widget = QListWidget()
self.list_widget.setToolTip("选择一个订阅后可立即更新或删除")
layout.addWidget(self.list_widget, 1)
self.ed_name = QLineEdit()
self.ed_name.setPlaceholderText("订阅名称,例如:我的备用节点")
layout.addWidget(self.ed_name)
self.ed_url = QLineEdit()
self.ed_url.setPlaceholderText("订阅链接(http:// 或 https://)")
self.ed_url.returnPressed.connect(self._add)
layout.addWidget(self.ed_url)
self.lb_status = QLabel("")
self.lb_status.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 11px;")
self.lb_status.setFixedHeight(16)
layout.addWidget(self.lb_status)
actions = QHBoxLayout()
self.btn_add = QPushButton("添加")
self.btn_add.setObjectName("btnOk")
self.btn_add.clicked.connect(self._add)
actions.addWidget(self.btn_add)
self.btn_refresh = QPushButton("立即更新")
self.btn_refresh.setObjectName("btnSmall")
self.btn_refresh.clicked.connect(self._refresh)
actions.addWidget(self.btn_refresh)
self.btn_delete = QPushButton("删除")
self.btn_delete.setObjectName("btnDanger")
self.btn_delete.clicked.connect(self._delete)
actions.addWidget(self.btn_delete)
actions.addStretch()
self.btn_close = QPushButton("关闭")
self.btn_close.setObjectName("btnSmall")
self.btn_close.clicked.connect(self.accept)
actions.addWidget(self.btn_close)
layout.addLayout(actions)
self._done.connect(self._on_done)
self._reload_list()
def _reload_list(self, selected_id=None):
self.list_widget.clear()
for item_data in self._services.load_subscriptions():
text = (f"{item_data['name']} · {item_data['node_count']} 个节点"
f" · {item_data.get('updated_at') or '未更新'}\n{item_data['url']}")
if item_data.get("last_error"):
text += f" · 上次更新失败:{item_data['last_error']}"
item = QListWidgetItem(text)
item.setData(Qt.UserRole, item_data["id"])
self.list_widget.addItem(item)
if selected_id and item_data["id"] == selected_id:
self.list_widget.setCurrentItem(item)
def _set_busy(self, busy, message=""):
self._busy = busy
for button in (self.btn_add, self.btn_refresh, self.btn_delete, self.btn_close):
button.setEnabled(not busy)
self.ed_name.setEnabled(not busy)
self.ed_url.setEnabled(not busy)
if message:
self.lb_status.setText(message)
self.lb_status.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 11px;")
def _add(self):
if self._busy:
return
name, url = self.ed_name.text().strip(), self.ed_url.text().strip()
if not name or not url:
self.lb_status.setText("请填写订阅名称和链接")
self.lb_status.setStyleSheet(f"color: {C_RED}; font-size: 11px;")
return
self._set_busy(True, "正在下载并校验订阅...")
threading.Thread(target=self._worker, args=("add", name, url), daemon=True).start()
def _selected_id(self):
item = self.list_widget.currentItem()
return item.data(Qt.UserRole) if item else None
def _refresh(self):
subscription_id = self._selected_id()
if not subscription_id or self._busy:
self.lb_status.setText("请先选择一个订阅")
self.lb_status.setStyleSheet(f"color: {C_RED}; font-size: 11px;")
return
self._set_busy(True, "正在更新订阅...")
threading.Thread(target=self._worker, args=("refresh", subscription_id, ""), daemon=True).start()
def _delete(self):
subscription_id = self._selected_id()
if not subscription_id or self._busy:
self.lb_status.setText("请先选择一个订阅")
self.lb_status.setStyleSheet(f"color: {C_RED}; font-size: 11px;")
return
self._services.remove_subscription(subscription_id)
self._reload_list()
self.lb_status.setText("已删除订阅")
self.lb_status.setStyleSheet(f"color: {C_GREEN}; font-size: 11px;")
def _worker(self, action, first, second):
try:
if action == "add":
self._services.add_subscription(first, second)
self._done.emit(action, True, "订阅已添加并导入")
else:
self._services.refresh_subscription(first)
self._done.emit(action, True, "订阅已更新")
except Exception as error:
self._done.emit(action, False, str(error))
def _on_done(self, action, ok, message):
self._set_busy(False)
if ok:
if action == "add":
self.ed_name.clear()
self.ed_url.clear()
self._reload_list(self._selected_id())
self.lb_status.setText(message)
color = C_GREEN if ok else C_RED
self.lb_status.setStyleSheet(f"color: {color}; font-size: 11px;")
@dataclass(frozen=True)
class SettingsServices:
"""设置框所需的配置与业务回调。"""
load_icon: object
version: str
get_port: object
get_update_via_proxy: object
get_skip_update_check: object
parse_port: object
set_port: object
set_update_via_proxy: object
set_skip_update_check: object
open_whitelist: object
open_third_party: object
has_login: object
open_login: object
logout: object
class SettingsDialog(QDialog):
"""代理端口、更新方式、白名单与登录状态设置框。"""
def __init__(self, services: SettingsServices, parent=None, port_locked=False):
super().__init__(parent)
self._services = services
self.setWindowTitle("设置")
self.setWindowIcon(services.load_icon())
self.setFixedSize(300, 396)
self.setStyleSheet(f"""
QDialog {{ background-color: {C_BG}; }}
QLabel {{ color: {C_TEXT}; font-family: "Microsoft YaHei", "Segoe UI", sans-serif; font-size: 12px; }}
QLineEdit {{ background-color: {C_CARD}; border: 1px solid {C_BORDER}; border-radius: 6px;
padding: 2px 8px; color: {C_TEXT}; font-size: 12px; }}
""")
layout = QVBoxLayout(self)
layout.setSpacing(12)
layout.setContentsMargins(20, 16, 20, 16)
title = QLabel("设置")
title.setStyleSheet(f"color: {C_TITLE}; font-size: 15px; font-weight: bold;")
title.setAlignment(Qt.AlignCenter)
layout.addWidget(title)
version_row = QHBoxLayout()
version_row.addStretch()
version_label = QLabel(f"版本号: {services.version}")
version_label.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 11px;")
version_row.addWidget(version_label)
version_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)
version_row.addWidget(self.link_check_update)
version_row.addStretch()
layout.addLayout(version_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()
whitelist_row = QHBoxLayout()
whitelist_label = QLabel("代理白名单")
whitelist_label.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 12px;")
whitelist_label.setToolTip("命中列表的域名 / IP 将强制直连(不经过代理),每条规则可单独启用")
whitelist_row.addWidget(whitelist_label)
whitelist_row.addStretch()
self.btn_whitelist = QPushButton("配置")
self.btn_whitelist.setCursor(Qt.PointingHandCursor)
self.btn_whitelist.setStyleSheet(f"""
QPushButton {{ background-color: {C_CARD}; color: {C_SUBTEXT}; border: 1px solid {C_BORDER};
border-radius: 6px; padding: 2px 10px; font-size: 11px; }}
QPushButton:hover {{ background-color: #e3e8ee; color: {C_TEXT}; }}
""")
self.btn_whitelist.clicked.connect(self._open_whitelist)
whitelist_row.addWidget(self.btn_whitelist)
layout.addLayout(whitelist_row)
third_party_row = QHBoxLayout()
third_party_label = QLabel("第三方")
third_party_label.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 12px;")
third_party_label.setToolTip("添加、更新或删除第三方 RRPP Client JSON 订阅")
third_party_row.addWidget(third_party_label)
third_party_row.addStretch()
self.btn_third_party = QPushButton("添加")
self.btn_third_party.setCursor(Qt.PointingHandCursor)
self.btn_third_party.setStyleSheet(self.btn_whitelist.styleSheet())
self.btn_third_party.clicked.connect(self._open_third_party)
third_party_row.addWidget(self.btn_third_party)
layout.addLayout(third_party_row)
port_row = QHBoxLayout()
port_label = QLabel("代理转发端口")
port_label.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 12px;")
port_row.addWidget(port_label)
port_row.addStretch()
self.ed_port = QLineEdit(str(services.get_port()))
self.ed_port.setFixedSize(54, 22)
self.ed_port.setEnabled(not port_locked)
port_row.addWidget(self.ed_port)
layout.addLayout(port_row)
update_row = QHBoxLayout()
update_label = QLabel("使用代理更新")
update_label.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 12px;")
update_row.addWidget(update_label)
update_row.addStretch()
self.cb_update_proxy = ToggleSwitch(checked=services.get_update_via_proxy())
self.cb_update_proxy.setToolTip("开启后检查更新与下载走节点隧道(CONNECT);默认直连仓库")
update_row.addWidget(self.cb_update_proxy)
layout.addLayout(update_row)
skip_row = QHBoxLayout()
skip_label = QLabel("跳过检查更新")
skip_label.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 12px;")
skip_row.addWidget(skip_label)
skip_row.addStretch()
self.cb_skip_update = ToggleSwitch(checked=services.get_skip_update_check())
self.cb_skip_update.setToolTip("跳过 PAC 数据与程序版本检查(节点更新与登录不受影响),被跳过的检查不输出日志")
skip_row.addWidget(self.cb_skip_update)
layout.addLayout(skip_row)
layout.addStretch()
buttons = QHBoxLayout()
buttons.setSpacing(10)
buttons.addStretch()
self.btn_save = QPushButton("保存更改")
self.btn_save.setStyleSheet(f"""
QPushButton {{ background-color: {C_GREEN}; color: #fff; border: none; border-radius: 6px;
padding: 5px 14px; font-size: 12px; font-weight: bold; }}
QPushButton:disabled {{ background-color: #2a3a2e; color: #666; }}
""")
self.btn_save.setEnabled(not port_locked)
self.btn_save.clicked.connect(self._save_settings)
buttons.addWidget(self.btn_save)
self.btn_logout = QPushButton("退出登录" if services.has_login() else "登录")
self.btn_logout.setStyleSheet(f"""
QPushButton {{ background-color: {C_RED}; color: #fff; border: none; border-radius: 6px;
padding: 5px 14px; font-size: 12px; font-weight: bold; }}
QPushButton:hover {{ background-color: #ff8787; }}
""")
self.btn_logout.setToolTip("登录获取官方节点;退出登录不会删除第三方订阅")
self.btn_logout.clicked.connect(self._logout)
buttons.addWidget(self.btn_logout)
buttons.addStretch()
layout.addLayout(buttons)
def _manual_check_update(self):
parent = self.parent()
if not (hasattr(parent, "_check_update") and hasattr(parent, "_update_done")):
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 _open_whitelist(self):
self._services.open_whitelist(self)
def _open_third_party(self):
self._services.open_third_party(self)
def _save_settings(self):
port = self._services.parse_port(self.ed_port.text())
if port is None:
self.lb_hint.setText("端口应为 1-65535 的数字")
self.lb_hint.setStyleSheet(f"color: {C_RED}; font-size: 11px;")
return
self._services.set_port(port)
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.ed_port.setText(str(port))
self.lb_hint.setText("已保存,下次开启代理生效")
self.lb_hint.setStyleSheet(f"color: {C_GREEN}; font-size: 11px;")
def _logout(self):
if self._services.has_login():
self._services.logout()
self.btn_logout.setText("登录")
else:
self._services.open_login(self)
if self._services.has_login():
self.btn_logout.setText("退出登录")