platform: modularize api/gui, add docs-tests-web foundation, and refresh root config
This commit is contained in:
428
selective-vpn-gui/main_window/singbox/runtime_profiles_mixin.py
Normal file
428
selective-vpn-gui/main_window/singbox/runtime_profiles_mixin.py
Normal file
@@ -0,0 +1,428 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QDialog,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
|
||||
class SingBoxRuntimeProfilesMixin:
|
||||
def on_singbox_profile_edit_dialog(self, cid: str = "") -> None:
|
||||
def work():
|
||||
target = str(cid or "").strip() or self._selected_transport_engine_id()
|
||||
if not target:
|
||||
raise RuntimeError("Select a transport engine first")
|
||||
if not self._select_transport_engine_by_id(target):
|
||||
raise RuntimeError(f"Transport engine '{target}' not found")
|
||||
|
||||
self._sync_selected_singbox_profile_link(silent=True)
|
||||
self._load_singbox_editor_for_selected(silent=False)
|
||||
client = self._selected_transport_client()
|
||||
pid = self._selected_singbox_profile_id()
|
||||
if client is None or not pid:
|
||||
raise RuntimeError("Select a SingBox profile first")
|
||||
|
||||
profile_name = self.ent_singbox_proto_name.text().strip() or str(getattr(client, "name", "") or pid).strip()
|
||||
host_layout = self.grp_singbox_profile_settings.layout()
|
||||
if host_layout is None:
|
||||
raise RuntimeError("internal layout is unavailable")
|
||||
editor = self.grp_singbox_proto_editor
|
||||
insert_at = host_layout.indexOf(editor)
|
||||
if insert_at >= 0:
|
||||
host_layout.removeWidget(editor)
|
||||
|
||||
moved = False
|
||||
dlg = QDialog(self)
|
||||
dlg.setModal(True)
|
||||
dlg.setWindowTitle(f"Edit SingBox profile: {profile_name}")
|
||||
dlg.resize(860, 680)
|
||||
dlg_layout = QVBoxLayout(dlg)
|
||||
try:
|
||||
hint = QLabel("Edit protocol fields and save draft. Use profile card menu for Run/Delete.")
|
||||
hint.setStyleSheet("color: gray;")
|
||||
dlg_layout.addWidget(hint)
|
||||
|
||||
editor.setTitle(f"{self._singbox_editor_default_title} · {profile_name}")
|
||||
editor.setParent(dlg)
|
||||
editor.setVisible(True)
|
||||
moved = True
|
||||
dlg_layout.addWidget(editor, stretch=1)
|
||||
|
||||
actions = QHBoxLayout()
|
||||
btn_save = QPushButton("Save draft")
|
||||
btn_close = QPushButton("Close")
|
||||
actions.addWidget(btn_save)
|
||||
actions.addStretch(1)
|
||||
actions.addWidget(btn_close)
|
||||
dlg_layout.addLayout(actions)
|
||||
|
||||
def save_draft_clicked() -> None:
|
||||
try:
|
||||
selected_client, _eid, selected_pid = self._selected_singbox_profile_context()
|
||||
saved = self._save_singbox_editor_draft(selected_client, profile_id=selected_pid)
|
||||
line = (saved.pretty_text or "").strip() or f"save profile {selected_pid}"
|
||||
self._append_transport_log(f"[profile] {line}")
|
||||
self.ctrl.log_gui(f"[singbox-profile] {line}")
|
||||
self.lbl_transport_engine_meta.setText(f"Engine: profile {selected_pid} draft saved")
|
||||
self.lbl_transport_engine_meta.setStyleSheet("color: green;")
|
||||
self._render_singbox_profile_cards()
|
||||
self._sync_singbox_profile_card_selection(self._selected_transport_engine_id())
|
||||
QMessageBox.information(dlg, "SingBox profile", line)
|
||||
except Exception as e:
|
||||
QMessageBox.critical(dlg, "SingBox profile save error", str(e))
|
||||
|
||||
btn_save.clicked.connect(save_draft_clicked)
|
||||
btn_close.clicked.connect(dlg.accept)
|
||||
dlg.exec()
|
||||
finally:
|
||||
if moved:
|
||||
dlg_layout.removeWidget(editor)
|
||||
editor.setParent(self.grp_singbox_profile_settings)
|
||||
editor.setTitle(self._singbox_editor_default_title)
|
||||
if insert_at >= 0:
|
||||
host_layout.insertWidget(insert_at, editor)
|
||||
else:
|
||||
host_layout.addWidget(editor)
|
||||
editor.setVisible(False)
|
||||
|
||||
self._safe(work, title="SingBox profile edit error")
|
||||
|
||||
def on_transport_engine_action(
|
||||
self,
|
||||
action: Literal["provision", "start", "stop", "restart"],
|
||||
) -> None:
|
||||
def work():
|
||||
cid = self._selected_transport_engine_id()
|
||||
if not cid:
|
||||
raise RuntimeError("Select a transport engine first")
|
||||
|
||||
self.lbl_transport_engine_meta.setText(f"Engine: {action} {cid}...")
|
||||
self.lbl_transport_engine_meta.setStyleSheet("color: orange;")
|
||||
QApplication.processEvents()
|
||||
|
||||
if action == "start":
|
||||
selected_client = self._selected_transport_client()
|
||||
if selected_client is not None and str(getattr(selected_client, "kind", "") or "").strip().lower() == "singbox":
|
||||
_client, _eid, pid = self._selected_singbox_profile_context()
|
||||
self.lbl_transport_engine_meta.setText(f"Engine: preparing profile {pid} for start...")
|
||||
self.lbl_transport_engine_meta.setStyleSheet("color: orange;")
|
||||
QApplication.processEvents()
|
||||
pre = self.ctrl.singbox_profile_apply_action(
|
||||
pid,
|
||||
client_id=cid,
|
||||
restart=False,
|
||||
skip_runtime=True,
|
||||
check_binary=True,
|
||||
client=selected_client,
|
||||
)
|
||||
pre_line = (pre.pretty_text or "").strip() or f"apply profile {pid}"
|
||||
self._append_transport_log(f"[profile] {pre_line}")
|
||||
self.ctrl.log_gui(f"[singbox-profile] {pre_line}")
|
||||
if not pre.ok:
|
||||
raise RuntimeError(f"profile preflight failed: {pre_line}")
|
||||
|
||||
ok, msg = self._apply_transport_switch_policy(cid)
|
||||
self._append_transport_log(f"[switch] {msg}")
|
||||
self.ctrl.log_gui(f"[transport-switch] {msg}")
|
||||
if not ok:
|
||||
if "canceled by user" in msg.lower():
|
||||
self.refresh_transport_engines(silent=True)
|
||||
return
|
||||
raise RuntimeError(msg)
|
||||
|
||||
res = self.ctrl.transport_client_action(cid, action if action != "start" else "start")
|
||||
line = (res.pretty_text or "").strip() or f"{action} {cid}"
|
||||
self._append_transport_log(f"[engine] {line}")
|
||||
self.ctrl.log_gui(f"[transport-engine] {line}")
|
||||
if not res.ok:
|
||||
raise RuntimeError(line)
|
||||
|
||||
self.refresh_transport_engines(silent=True)
|
||||
self.refresh_status_tab()
|
||||
|
||||
self._safe(work, title="Transport engine error")
|
||||
|
||||
def on_transport_engine_delete(self, cid: str = "") -> None:
|
||||
def work():
|
||||
target = str(cid or "").strip() or self._selected_transport_engine_id()
|
||||
if not target:
|
||||
raise RuntimeError("Select a transport engine first")
|
||||
if not self._select_transport_engine_by_id(target):
|
||||
raise RuntimeError(f"Transport engine '{target}' not found")
|
||||
|
||||
ans = QMessageBox.question(
|
||||
self,
|
||||
"Delete transport profile",
|
||||
(
|
||||
f"Delete profile '{target}'?\n\n"
|
||||
"The client configuration and related runtime artifacts will be removed."
|
||||
),
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No,
|
||||
)
|
||||
if ans != QMessageBox.Yes:
|
||||
return
|
||||
|
||||
self.lbl_transport_engine_meta.setText(f"Engine: deleting {target}...")
|
||||
self.lbl_transport_engine_meta.setStyleSheet("color: orange;")
|
||||
QApplication.processEvents()
|
||||
|
||||
res = self.ctrl.transport_client_delete_action(target, force=False, cleanup=True)
|
||||
if not res.ok and "force=true" in (res.pretty_text or "").lower():
|
||||
force_ans = QMessageBox.question(
|
||||
self,
|
||||
"Profile is referenced",
|
||||
(
|
||||
"This profile is referenced by current transport policy.\n"
|
||||
"Force delete anyway?"
|
||||
),
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No,
|
||||
)
|
||||
if force_ans == QMessageBox.Yes:
|
||||
res = self.ctrl.transport_client_delete_action(target, force=True, cleanup=True)
|
||||
else:
|
||||
self._append_transport_log(f"[engine] delete {target}: canceled by user")
|
||||
self.ctrl.log_gui(f"[transport-engine] delete {target}: canceled by user")
|
||||
return
|
||||
|
||||
line = (res.pretty_text or "").strip() or f"delete {target}"
|
||||
self._append_transport_log(f"[engine] {line}")
|
||||
self.ctrl.log_gui(f"[transport-engine] {line}")
|
||||
if not res.ok:
|
||||
raise RuntimeError(line)
|
||||
|
||||
self.refresh_transport_engines(silent=True)
|
||||
self.refresh_status_tab()
|
||||
|
||||
self._safe(work, title="Transport engine delete error")
|
||||
|
||||
def on_transport_policy_rollback(self) -> None:
|
||||
def work():
|
||||
self.lbl_transport_engine_meta.setText("Engine: rollback policy...")
|
||||
self.lbl_transport_engine_meta.setStyleSheet("color: orange;")
|
||||
QApplication.processEvents()
|
||||
|
||||
res = self.ctrl.transport_policy_rollback_action()
|
||||
line = (res.pretty_text or "").strip() or "policy rollback"
|
||||
self._append_transport_log(f"[switch] {line}")
|
||||
self.ctrl.log_gui(f"[transport-switch] {line}")
|
||||
if not res.ok:
|
||||
raise RuntimeError(line)
|
||||
|
||||
self.refresh_transport_engines(silent=True)
|
||||
self.refresh_status_tab()
|
||||
|
||||
self._safe(work, title="Transport rollback error")
|
||||
|
||||
def on_toggle_singbox_profile_settings(self, checked: bool = False) -> None:
|
||||
if checked and self.btn_singbox_toggle_global_defaults.isChecked():
|
||||
self.btn_singbox_toggle_global_defaults.setChecked(False)
|
||||
self._apply_singbox_compact_visibility()
|
||||
self._save_ui_preferences()
|
||||
|
||||
def on_toggle_singbox_global_defaults(self, checked: bool = False) -> None:
|
||||
if checked and self.btn_singbox_toggle_profile_settings.isChecked():
|
||||
self.btn_singbox_toggle_profile_settings.setChecked(False)
|
||||
self._apply_singbox_compact_visibility()
|
||||
self._save_ui_preferences()
|
||||
|
||||
def on_toggle_singbox_activity(self, _checked: bool = False) -> None:
|
||||
self._apply_singbox_compact_visibility()
|
||||
self._save_ui_preferences()
|
||||
|
||||
def on_singbox_profile_scope_changed(self, _state: int = 0) -> None:
|
||||
self._apply_singbox_profile_controls()
|
||||
self._save_ui_preferences()
|
||||
self._update_transport_engine_view()
|
||||
|
||||
def on_singbox_global_defaults_changed(self, _index: int = 0) -> None:
|
||||
self._refresh_singbox_profile_effective()
|
||||
self._save_ui_preferences()
|
||||
self._update_transport_engine_view()
|
||||
|
||||
def on_singbox_global_save(self) -> None:
|
||||
def work():
|
||||
self._save_ui_preferences()
|
||||
route, dns, killswitch = self._effective_singbox_policy()
|
||||
msg = (
|
||||
"Global defaults saved: "
|
||||
f"routing={self._singbox_value_label('routing', route)}, "
|
||||
f"dns={self._singbox_value_label('dns', dns)}, "
|
||||
f"kill-switch={self._singbox_value_label('killswitch', killswitch)}"
|
||||
)
|
||||
self._append_transport_log(f"[profile] {msg}")
|
||||
self.ctrl.log_gui(f"[singbox-settings] {msg}")
|
||||
self._safe(work, title="SingBox settings error")
|
||||
|
||||
def on_singbox_profile_save(self) -> None:
|
||||
def work():
|
||||
client, eid, pid = self._selected_singbox_profile_context()
|
||||
self._save_ui_preferences()
|
||||
self.lbl_transport_engine_meta.setText(f"Engine: saving draft for {pid}...")
|
||||
self.lbl_transport_engine_meta.setStyleSheet("color: orange;")
|
||||
QApplication.processEvents()
|
||||
|
||||
saved = self._save_singbox_editor_draft(client, profile_id=pid)
|
||||
save_line = (saved.pretty_text or "").strip() or f"save profile {pid}"
|
||||
self._append_transport_log(f"[profile] {save_line}")
|
||||
self.ctrl.log_gui(f"[singbox-profile] {save_line}")
|
||||
|
||||
route, dns, killswitch = self._effective_singbox_policy()
|
||||
msg = (
|
||||
f"profile settings saved for {eid}: "
|
||||
f"routing={self._singbox_value_label('routing', route)}, "
|
||||
f"dns={self._singbox_value_label('dns', dns)}, "
|
||||
f"kill-switch={self._singbox_value_label('killswitch', killswitch)}"
|
||||
)
|
||||
self._append_transport_log(f"[profile] {msg}")
|
||||
self.ctrl.log_gui(f"[singbox-profile] {msg}")
|
||||
self.lbl_transport_engine_meta.setText(f"Engine: profile {pid} draft saved")
|
||||
self.lbl_transport_engine_meta.setStyleSheet("color: green;")
|
||||
self.refresh_transport_engines(silent=True)
|
||||
self._safe(work, title="SingBox profile save error")
|
||||
|
||||
def _selected_singbox_profile_context(self):
|
||||
client = self._selected_transport_client()
|
||||
eid = self._selected_transport_engine_id()
|
||||
pid = self._selected_singbox_profile_id()
|
||||
if not eid or client is None:
|
||||
raise RuntimeError("Select a transport engine first")
|
||||
if not pid:
|
||||
raise RuntimeError("Select a SingBox profile first")
|
||||
return client, eid, pid
|
||||
|
||||
def _run_singbox_profile_action(
|
||||
self,
|
||||
*,
|
||||
verb: str,
|
||||
runner,
|
||||
refresh_status: bool = False,
|
||||
sync_draft: bool = False,
|
||||
) -> None:
|
||||
client, eid, pid = self._selected_singbox_profile_context()
|
||||
if sync_draft:
|
||||
self.lbl_transport_engine_meta.setText(f"Engine: syncing draft for {pid}...")
|
||||
self.lbl_transport_engine_meta.setStyleSheet("color: orange;")
|
||||
QApplication.processEvents()
|
||||
saved = self._save_singbox_editor_draft(client, profile_id=pid)
|
||||
save_line = (saved.pretty_text or "").strip() or f"save profile {pid}"
|
||||
self._append_transport_log(f"[profile] {save_line}")
|
||||
self.ctrl.log_gui(f"[singbox-profile] {save_line}")
|
||||
|
||||
self.lbl_transport_engine_meta.setText(f"Engine: {verb} profile {pid}...")
|
||||
self.lbl_transport_engine_meta.setStyleSheet("color: orange;")
|
||||
QApplication.processEvents()
|
||||
|
||||
res = runner(client, eid, pid)
|
||||
line = (res.pretty_text or "").strip() or f"{verb} profile {pid}"
|
||||
self._append_transport_log(f"[profile] {line}")
|
||||
self.ctrl.log_gui(f"[singbox-profile] {line}")
|
||||
|
||||
if res.ok:
|
||||
self.lbl_transport_engine_meta.setText(f"Engine: profile {pid} {verb} done")
|
||||
self.lbl_transport_engine_meta.setStyleSheet("color: green;")
|
||||
else:
|
||||
self.lbl_transport_engine_meta.setText(f"Engine: profile {pid} {verb} failed")
|
||||
self.lbl_transport_engine_meta.setStyleSheet("color: orange;")
|
||||
|
||||
self.refresh_transport_engines(silent=True)
|
||||
if refresh_status:
|
||||
self.refresh_status_tab()
|
||||
|
||||
def on_singbox_profile_preview(self) -> None:
|
||||
self._safe(
|
||||
lambda: self._run_singbox_profile_action(
|
||||
verb="previewing",
|
||||
runner=lambda client, _eid, pid: self.ctrl.singbox_profile_render_preview_action(
|
||||
pid,
|
||||
check_binary=None,
|
||||
persist=False,
|
||||
client=client,
|
||||
),
|
||||
refresh_status=False,
|
||||
sync_draft=True,
|
||||
),
|
||||
title="SingBox profile preview error",
|
||||
)
|
||||
|
||||
def on_singbox_profile_validate(self) -> None:
|
||||
self._safe(
|
||||
lambda: self._run_singbox_profile_action(
|
||||
verb="validating",
|
||||
runner=lambda client, _eid, pid: self.ctrl.singbox_profile_validate_action(
|
||||
pid,
|
||||
client=client,
|
||||
),
|
||||
refresh_status=False,
|
||||
sync_draft=True,
|
||||
),
|
||||
title="SingBox profile validate error",
|
||||
)
|
||||
|
||||
def on_singbox_profile_apply(self) -> None:
|
||||
self._safe(
|
||||
lambda: self._run_singbox_profile_action(
|
||||
verb="applying",
|
||||
runner=lambda client, eid, pid: self.ctrl.singbox_profile_apply_action(
|
||||
pid,
|
||||
client_id=eid,
|
||||
restart=True,
|
||||
skip_runtime=False,
|
||||
client=client,
|
||||
),
|
||||
refresh_status=True,
|
||||
sync_draft=True,
|
||||
),
|
||||
title="SingBox profile apply error",
|
||||
)
|
||||
|
||||
def on_singbox_profile_rollback(self) -> None:
|
||||
self._safe(
|
||||
lambda: self._run_singbox_profile_action(
|
||||
verb="rolling back",
|
||||
runner=lambda client, eid, pid: self.ctrl.singbox_profile_rollback_action(
|
||||
pid,
|
||||
client_id=eid,
|
||||
restart=True,
|
||||
skip_runtime=False,
|
||||
client=client,
|
||||
),
|
||||
refresh_status=True,
|
||||
),
|
||||
title="SingBox profile rollback error",
|
||||
)
|
||||
|
||||
def on_singbox_profile_history(self) -> None:
|
||||
def work():
|
||||
client, _eid, pid = self._selected_singbox_profile_context()
|
||||
self.lbl_transport_engine_meta.setText(f"Engine: loading history for {pid}...")
|
||||
self.lbl_transport_engine_meta.setStyleSheet("color: orange;")
|
||||
QApplication.processEvents()
|
||||
|
||||
lines = self.ctrl.singbox_profile_history_lines(pid, limit=20, client=client)
|
||||
if not lines:
|
||||
line = f"history profile {pid}: no entries"
|
||||
self._append_transport_log(f"[history] {line}")
|
||||
self.ctrl.log_gui(f"[singbox-profile] {line}")
|
||||
self.lbl_transport_engine_meta.setText(f"Engine: history {pid} is empty")
|
||||
self.lbl_transport_engine_meta.setStyleSheet("color: gray;")
|
||||
return
|
||||
|
||||
header = f"history profile {pid}: {len(lines)} entries"
|
||||
self._append_transport_log(f"[history] {header}")
|
||||
self.ctrl.log_gui(f"[singbox-profile] {header}")
|
||||
for ln in lines:
|
||||
self._append_transport_log(f"[history] {ln}")
|
||||
self.lbl_transport_engine_meta.setText(f"Engine: history loaded for {pid}")
|
||||
self.lbl_transport_engine_meta.setStyleSheet("color: green;")
|
||||
|
||||
self._safe(work, title="SingBox profile history error")
|
||||
Reference in New Issue
Block a user