feat: 支持第三方节点订阅
This commit is contained in:
+200
-6
@@ -103,8 +103,9 @@ class LoginDialog(QDialog):
|
||||
hint.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 10px;")
|
||||
bottom.addWidget(hint)
|
||||
bottom.addStretch()
|
||||
self.btn_cancel = QPushButton("取消")
|
||||
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("登录")
|
||||
@@ -291,6 +292,175 @@ class WhitelistDialog(QDialog):
|
||||
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:
|
||||
"""设置框所需的配置与业务回调。"""
|
||||
@@ -305,6 +475,9 @@ class SettingsServices:
|
||||
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
|
||||
|
||||
|
||||
@@ -316,7 +489,7 @@ class SettingsDialog(QDialog):
|
||||
self._services = services
|
||||
self.setWindowTitle("设置")
|
||||
self.setWindowIcon(services.load_icon())
|
||||
self.setFixedSize(300, 354)
|
||||
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; }}
|
||||
@@ -371,6 +544,19 @@ class SettingsDialog(QDialog):
|
||||
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;")
|
||||
@@ -415,13 +601,13 @@ class SettingsDialog(QDialog):
|
||||
self.btn_save.setEnabled(not port_locked)
|
||||
self.btn_save.clicked.connect(self._save_settings)
|
||||
buttons.addWidget(self.btn_save)
|
||||
self.btn_logout = QPushButton("退出登录")
|
||||
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.setToolTip("登录获取官方节点;退出登录不会删除第三方订阅")
|
||||
self.btn_logout.clicked.connect(self._logout)
|
||||
buttons.addWidget(self.btn_logout)
|
||||
buttons.addStretch()
|
||||
@@ -450,6 +636,9 @@ class SettingsDialog(QDialog):
|
||||
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:
|
||||
@@ -464,5 +653,10 @@ class SettingsDialog(QDialog):
|
||||
self.lb_hint.setStyleSheet(f"color: {C_GREEN}; font-size: 11px;")
|
||||
|
||||
def _logout(self):
|
||||
self._services.logout()
|
||||
self.accept()
|
||||
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("退出登录")
|
||||
|
||||
Reference in New Issue
Block a user