zapret-kvn/xray_fluent/ui/updates_page.py
loop-uh 0b559eb7e6
Some checks failed
Windows project source guards / test (push) Has been cancelled
Update the sing-box core and let the button apply a subscription
The subscription button read the source and applied nothing, so fresh servers
existed only in its report while the table kept the previous count. That read as
lost servers rather than as work not yet done. The button now applies what it
downloads; checking without applying stayed in the context menu, where it says
so.

The sing-box Extended core only ever arrived with a build of the application, so
a fix in the core had to wait for the next Zapret KVN release. The updates page
offers it the same way it offers Xray: show the installed version, look up the
newest release, and replace the executable only on an explicit action. Its tag
carries two versions — the core and the extension set — and both are compared,
otherwise an update within the same core version would go unnoticed. A release
that cannot be read is never treated as newer, and an archive without the
executable leaves the previous core in place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 20:35:51 +03:00

250 lines
9.5 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 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 = 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:
if kind == "success":
light, dark = _status_qss("success", 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)
# ── 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")