platform: modularize api/gui, add docs-tests-web foundation, and refresh root config

This commit is contained in:
beckline
2026-03-26 22:40:54 +03:00
parent 0e2d7f61ea
commit 6a56d734c2
562 changed files with 70151 additions and 16423 deletions

View File

@@ -0,0 +1,244 @@
from __future__ import annotations
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QFormLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QLineEdit,
QPlainTextEdit,
QListView,
QPushButton,
QStackedWidget,
QStyle,
QTabWidget,
QToolButton,
QVBoxLayout,
QWidget,
QComboBox,
)
class UITabsMainMixin:
def _build_ui(self) -> None:
root = QWidget()
root_layout = QVBoxLayout(root)
root.setLayout(root_layout)
self.setCentralWidget(root)
# top bar ---------------------------------------------------------
top = QHBoxLayout()
root_layout.addLayout(top)
# клик по этому баннеру показывает whoami
self.btn_login_banner = QPushButton("AdGuard VPN: —")
self.btn_login_banner.setFlat(True)
self.btn_login_banner.setStyleSheet(
"text-align: left; border: none; color: gray;"
)
self.btn_login_banner.clicked.connect(self.on_login_banner_clicked)
top.addWidget(self.btn_login_banner, stretch=1)
self.btn_auth = QPushButton("Login")
self.btn_auth.clicked.connect(self.on_auth_button)
top.addWidget(self.btn_auth)
self.btn_refresh_all = QPushButton("Refresh all")
self.btn_refresh_all.clicked.connect(self.refresh_everything)
top.addWidget(self.btn_refresh_all)
# tabs -------------------------------------------------------------
self.tabs = QTabWidget()
root_layout.addWidget(self.tabs, stretch=1)
self._build_tab_status()
self._build_tab_vpn()
self._build_tab_singbox()
self._build_tab_multiif()
self._build_tab_routes()
self._build_tab_dns()
self._build_tab_domains()
self._build_tab_trace()
# ---------------- STATUS TAB ----------------
def _build_tab_status(self) -> None:
tab = QWidget()
layout = QVBoxLayout(tab)
grid = QFormLayout()
layout.addLayout(grid)
self.st_timestamp = QLabel("")
self.st_counts = QLabel("")
self.st_iface = QLabel("")
self.st_route = QLabel("")
self.st_routes_service = QLabel("")
self.st_smartdns_service = QLabel("")
self.st_vpn_service = QLabel("")
grid.addRow("Timestamp:", self.st_timestamp)
grid.addRow("Counts:", self.st_counts)
grid.addRow("Iface / table / mark:", self.st_iface)
grid.addRow("Policy route:", self.st_route)
grid.addRow("Routes service:", self.st_routes_service)
grid.addRow("SmartDNS:", self.st_smartdns_service)
grid.addRow("VPN service:", self.st_vpn_service)
btns = QHBoxLayout()
layout.addLayout(btns)
btn_refresh = QPushButton("Refresh")
btn_refresh.clicked.connect(self.refresh_status_tab)
btns.addWidget(btn_refresh)
btns.addStretch(1)
self.tabs.addTab(tab, "Status")
# ---------------- VPN TAB ----------------
def _build_tab_vpn(self) -> None:
tab = QWidget()
self.tab_vpn = tab # нужно, чтобы переключаться сюда из шапки
layout = QVBoxLayout(tab)
# stack: main vs login-flow page
self.vpn_stack = QStackedWidget()
layout.addWidget(self.vpn_stack, stretch=1)
# ---- main page
page_main = QWidget()
main_layout = QVBoxLayout(page_main)
# Autoconnect group
auto_group = QGroupBox("Autoconnect (AdGuardVPN autoloop)")
auto_layout = QHBoxLayout(auto_group)
self.btn_autoconnect_toggle = QPushButton("Enable autoconnect")
self.btn_autoconnect_toggle.clicked.connect(self.on_toggle_autoconnect)
auto_layout.addWidget(self.btn_autoconnect_toggle)
auto_layout.addStretch(1)
# справа текст "unit: active/inactive" с цветом
self.lbl_autoconnect_state = QLabel("unit: —")
self.lbl_autoconnect_state.setStyleSheet("color: gray;")
auto_layout.addWidget(self.lbl_autoconnect_state)
main_layout.addWidget(auto_group)
# Locations group
loc_group = QGroupBox("Location")
loc_layout = QVBoxLayout(loc_group)
loc_row = QHBoxLayout()
loc_layout.addLayout(loc_row)
self.cmb_locations = QComboBox()
# компактный popup со скроллом, а не на весь экран
self.cmb_locations.setMaxVisibleItems(12)
self.cmb_locations.setStyleSheet("combobox-popup: 0;")
self.cmb_locations.setFocusPolicy(Qt.StrongFocus)
view = QListView()
view.setUniformItemSizes(True)
self.cmb_locations.setView(view)
self.cmb_locations.activated.connect(self.on_location_activated)
self.cmb_locations.installEventFilter(self)
view.installEventFilter(self)
loc_row.addWidget(self.cmb_locations, stretch=1)
self.cmb_locations_sort = QComboBox()
self.cmb_locations_sort.addItem("Sort: Ping", "ping")
self.cmb_locations_sort.addItem("Sort: Ping (slow first)", "ping_desc")
self.cmb_locations_sort.addItem("Sort: Name", "name")
self.cmb_locations_sort.addItem("Sort: Name (Z-A)", "name_desc")
self.cmb_locations_sort.currentIndexChanged.connect(
self.on_locations_sort_changed
)
loc_row.addWidget(self.cmb_locations_sort)
self.btn_locations_refresh = QToolButton()
self.btn_locations_refresh.setAutoRaise(True)
self.btn_locations_refresh.setIcon(
self.style().standardIcon(QStyle.SP_BrowserReload)
)
self.btn_locations_refresh.setToolTip("Refresh locations now")
self.btn_locations_refresh.setCursor(Qt.PointingHandCursor)
self.btn_locations_refresh.setFocusPolicy(Qt.NoFocus)
self.btn_locations_refresh.clicked.connect(self.on_locations_refresh_click)
loc_row.addWidget(self.btn_locations_refresh)
self.lbl_locations_meta = QLabel("Locations: loading...")
self.lbl_locations_meta.setStyleSheet("color: gray;")
loc_layout.addWidget(self.lbl_locations_meta)
self.lbl_vpn_egress = QLabel("Egress: n/a")
self.lbl_vpn_egress.setStyleSheet("color: gray;")
loc_layout.addWidget(self.lbl_vpn_egress)
main_layout.addWidget(loc_group)
# Status output
self.txt_vpn = QPlainTextEdit()
self.txt_vpn.setReadOnly(True)
main_layout.addWidget(self.txt_vpn, stretch=1)
self.vpn_stack.addWidget(page_main)
# ---- login page
page_login = QWidget()
lf_layout = QVBoxLayout(page_login)
top = QHBoxLayout()
lf_layout.addLayout(top)
self.lbl_login_flow_status = QLabel("Status: —")
top.addWidget(self.lbl_login_flow_status)
self.lbl_login_flow_email = QLabel("")
self.lbl_login_flow_email.setStyleSheet("color: gray;")
top.addWidget(self.lbl_login_flow_email)
top.addStretch(1)
# URL + buttons row
row2 = QHBoxLayout()
lf_layout.addLayout(row2)
row2.addWidget(QLabel("URL:"))
self.edit_login_url = QLineEdit()
row2.addWidget(self.edit_login_url, stretch=1)
self.btn_login_open = QPushButton("Open")
self.btn_login_open.clicked.connect(self.on_login_open)
row2.addWidget(self.btn_login_open)
self.btn_login_copy = QPushButton("Copy")
self.btn_login_copy.clicked.connect(self.on_login_copy)
row2.addWidget(self.btn_login_copy)
self.btn_login_check = QPushButton("Check")
self.btn_login_check.clicked.connect(self.on_login_check)
row2.addWidget(self.btn_login_check)
self.btn_login_close = QPushButton("Cancel")
self.btn_login_close.clicked.connect(self.on_login_cancel)
row2.addWidget(self.btn_login_close)
self.btn_login_stop = QPushButton("Stop session")
self.btn_login_stop.clicked.connect(self.on_login_stop)
row2.addWidget(self.btn_login_stop)
# log text
self.txt_login_flow = QPlainTextEdit()
self.txt_login_flow.setReadOnly(True)
lf_layout.addWidget(self.txt_login_flow, stretch=1)
# bottom buttons
bottom = QHBoxLayout()
lf_layout.addLayout(bottom)
# Start login визуально убираем, но объект оставим на всякий
self.btn_login_start = QPushButton("Start login")
self.btn_login_start.clicked.connect(self.on_start_login)
self.btn_login_start.setVisible(False)
bottom.addWidget(self.btn_login_start)
btn_back = QPushButton("Back to VPN")
btn_back.clicked.connect(lambda: self._show_vpn_page("main"))
bottom.addWidget(btn_back)
bottom.addStretch(1)
self.vpn_stack.addWidget(page_login)
self.tabs.addTab(tab, "AdGuardVPN")