zapret-kvn/xray_fluent/ui/updates_page.py
loop-uh 3ac9d59c0c
Some checks failed
Windows project source guards / test (push) Failing after 40s
Тема: позитивные статусы в цвете акцента
Новый токен positive_color()/positive_pair() — акцент, подстроенный под
читаемость (в тёмной теме тёмный акцент осветляется, в светлой светлый
затемняется). В цвет акцента теперь: «актуален»/«последняя версия» на
«Обновлениях», «Подключено» и сфера на панели, живой пинг в таблице,
«Работает» у Zapret, «рекомендуется» в стратегиях, VPN-трафик процессов.
Ошибки и предупреждения остаются красными/оранжевыми. Метки со стилями
перекрашиваются при смене акцента.
2026-09-26 15:16:04 +03:00

258 lines
10 KiB
Python
Raw Permalink 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.

from __future__ import annotations
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout, QWidget
from qfluentwidgets import (
BodyLabel,
CaptionLabel,
FluentIcon as FIF,
IndeterminateProgressBar,
PrimaryPushButton,
ProgressBar,
PushButton,
SubtitleLabel,
TitleLabel,
setCustomStyleSheet,
)
from ..constants import APP_VERSION
from .theme import on_accent_changed, positive_pair, token_pair
_SECTION_TITLE_QSS = "BodyLabel { font-weight: bold; font-size: 16px; }"
def _status_qss(token: str, bold: bool) -> tuple[str, str]:
"""Build (light, dark) qss for a status CaptionLabel from a theme token."""
light, dark = positive_pair() if token == "positive" else token_pair(token)
extra = " font-weight: bold;" if bold else ""
return (
f"CaptionLabel {{ color: {light};{extra} }}",
f"CaptionLabel {{ color: {dark};{extra} }}",
)
def _set_status_style(label: CaptionLabel, kind: str) -> None:
# Вид запоминается на метке, чтобы при смене акцента перекрасить её.
label.setProperty("statusKind", kind)
if kind == "success":
light, dark = _status_qss("positive", bold=True)
elif kind == "error":
light, dark = _status_qss("error", bold=True)
else:
light, dark = _status_qss("text_muted", bold=False)
setCustomStyleSheet(label, light, dark)
class UpdatesPage(QWidget):
check_app_requested = pyqtSignal()
check_xray_requested = pyqtSignal()
update_xray_requested = pyqtSignal()
check_singbox_requested = pyqtSignal()
update_singbox_requested = pyqtSignal()
def __init__(self, parent: QWidget | None = None):
super().__init__(parent)
self.setObjectName("updates")
root = QVBoxLayout(self)
root.setContentsMargins(36, 28, 36, 28)
root.setSpacing(20)
title = SubtitleLabel("Обновления", self)
root.addWidget(title)
# ── App version info ──
app_box = QVBoxLayout()
app_box.setSpacing(6)
app_title = BodyLabel("zapret kvn", self)
setCustomStyleSheet(app_title, _SECTION_TITLE_QSS, _SECTION_TITLE_QSS)
app_box.addWidget(app_title)
self._app_version_label = BodyLabel(f"Текущая версия: v{APP_VERSION}", self)
app_box.addWidget(self._app_version_label)
self._app_status = CaptionLabel("", self)
_set_status_style(self._app_status, "neutral")
app_box.addWidget(self._app_status)
# Progress bar
self._app_progress = ProgressBar(self)
self._app_progress.setFixedHeight(4)
self._app_progress.setValue(0)
self._app_progress.hide()
app_box.addWidget(self._app_progress)
self._app_spinner = IndeterminateProgressBar(self)
self._app_spinner.setFixedHeight(4)
self._app_spinner.hide()
app_box.addWidget(self._app_spinner)
btn_row = QHBoxLayout()
btn_row.setSpacing(10)
self.check_app_btn = PrimaryPushButton(FIF.SYNC, "Проверить обновления", self)
self.download_btn = PushButton(FIF.DOWNLOAD, "Скачать и установить", self)
self.download_btn.hide()
btn_row.addWidget(self.check_app_btn)
btn_row.addWidget(self.download_btn)
btn_row.addStretch()
app_box.addLayout(btn_row)
root.addLayout(app_box)
# Separator
sep = QWidget(self)
sep.setFixedHeight(1)
sep.setStyleSheet("background-color: rgba(128,128,128,0.3);")
root.addWidget(sep)
# ── Xray core info ──
xray_box = QVBoxLayout()
xray_box.setSpacing(6)
xray_title = BodyLabel("Xray Core", self)
setCustomStyleSheet(xray_title, _SECTION_TITLE_QSS, _SECTION_TITLE_QSS)
xray_box.addWidget(xray_title)
self._xray_version_label = BodyLabel("Версия: загрузка...", self)
xray_box.addWidget(self._xray_version_label)
self._xray_status = CaptionLabel("", self)
_set_status_style(self._xray_status, "neutral")
xray_box.addWidget(self._xray_status)
xray_btn_row = QHBoxLayout()
xray_btn_row.setSpacing(10)
self.check_xray_btn = PushButton(FIF.SYNC, "Проверить обновления Xray", self)
self.update_xray_btn = PrimaryPushButton(FIF.DOWNLOAD, "Обновить Xray core", self)
xray_btn_row.addWidget(self.check_xray_btn)
xray_btn_row.addWidget(self.update_xray_btn)
xray_btn_row.addStretch()
xray_box.addLayout(xray_btn_row)
root.addLayout(xray_box)
singbox_sep = QWidget(self)
singbox_sep.setFixedHeight(1)
singbox_sep.setStyleSheet("background-color: rgba(128,128,128,0.3);")
root.addWidget(singbox_sep)
# ── sing-box core info ──
# Ядро приезжало только со сборкой приложения, поэтому исправление в нём
# приходилось ждать до следующего релиза Zapret KVN.
singbox_box = QVBoxLayout()
singbox_box.setSpacing(6)
singbox_title = BodyLabel("sing-box Extended", self)
setCustomStyleSheet(singbox_title, _SECTION_TITLE_QSS, _SECTION_TITLE_QSS)
singbox_box.addWidget(singbox_title)
self._singbox_version_label = BodyLabel("Версия: загрузка...", self)
singbox_box.addWidget(self._singbox_version_label)
self._singbox_status = CaptionLabel("", self)
_set_status_style(self._singbox_status, "neutral")
singbox_box.addWidget(self._singbox_status)
singbox_btn_row = QHBoxLayout()
singbox_btn_row.setSpacing(10)
self.check_singbox_btn = PushButton(FIF.SYNC, "Проверить обновления sing-box", self)
self.update_singbox_btn = PrimaryPushButton(FIF.DOWNLOAD, "Обновить sing-box core", self)
singbox_btn_row.addWidget(self.check_singbox_btn)
singbox_btn_row.addWidget(self.update_singbox_btn)
singbox_btn_row.addStretch()
singbox_box.addLayout(singbox_btn_row)
root.addLayout(singbox_box)
root.addStretch()
# ── Connections ──
self.check_app_btn.clicked.connect(self.check_app_requested)
self.check_xray_btn.clicked.connect(self.check_xray_requested)
self.update_xray_btn.clicked.connect(self.update_xray_requested)
self.check_singbox_btn.clicked.connect(self.check_singbox_requested)
self.update_singbox_btn.clicked.connect(self.update_singbox_requested)
on_accent_changed(self._restyle_statuses)
def _restyle_statuses(self, *_args) -> None:
"""Позитивный цвет идёт от акцента — перекрасить при его смене."""
for label in (self._app_status, self._xray_status, self._singbox_status):
_set_status_style(label, label.property("statusKind") or "neutral")
# ── Public API ──
def set_xray_version(self, version: str) -> None:
self._xray_version_label.setText(f"Версия: {version}" if version else "Версия: не найдена")
def set_app_status(self, text: str) -> None:
_set_status_style(self._app_status, "neutral")
self._app_status.setText(text)
def set_xray_status(self, text: str) -> None:
_set_status_style(self._xray_status, "neutral")
self._xray_status.setText(text)
def set_singbox_version(self, version: str) -> None:
self._singbox_version_label.setText(
f"Версия: {version}" if version else "Версия: не найдена"
)
def set_singbox_status(self, text: str) -> None:
_set_status_style(self._singbox_status, "neutral")
self._singbox_status.setText(text)
def set_singbox_error(self, text: str) -> None:
_set_status_style(self._singbox_status, "error")
self._singbox_status.setText(text)
def set_singbox_success(self, text: str) -> None:
_set_status_style(self._singbox_status, "success")
self._singbox_status.setText(text)
def set_app_error(self, text: str) -> None:
_set_status_style(self._app_status, "error")
self._app_status.setText(text)
def set_xray_error(self, text: str) -> None:
_set_status_style(self._xray_status, "error")
self._xray_status.setText(text)
def set_xray_success(self, text: str) -> None:
_set_status_style(self._xray_status, "success")
self._xray_status.setText(text)
def show_checking(self) -> None:
self._app_progress.hide()
self._app_spinner.show()
self._app_spinner.start()
self.check_app_btn.setEnabled(False)
_set_status_style(self._app_status, "neutral")
self._app_status.setText("Проверка обновлений...")
def show_download_progress(self, percent: int) -> None:
self._app_spinner.hide()
self._app_progress.show()
self._app_progress.setValue(percent)
_set_status_style(self._app_status, "neutral")
self._app_status.setText(f"Загрузка: {percent}%")
self.check_app_btn.setEnabled(False)
self.download_btn.setEnabled(False)
def show_idle(self) -> None:
self._app_spinner.stop()
self._app_spinner.hide()
self._app_progress.hide()
self._app_progress.setValue(0)
self.check_app_btn.setEnabled(True)
self.download_btn.hide()
def show_update_available(self, version: str) -> None:
self._app_spinner.stop()
self._app_spinner.hide()
self.check_app_btn.setEnabled(True)
self._app_status.setText(f"Доступна новая версия: v{version}")
_set_status_style(self._app_status, "success")
self.download_btn.show()
self.download_btn.setText(f"Скачать v{version} и установить")
def show_up_to_date(self) -> None:
self.show_idle()
self._app_status.setText("У вас последняя версия")
_set_status_style(self._app_status, "success")