feat: improve updater flow and release channels
Some checks failed
Desktop Dev Pre-release / prerelease (push) Failing after 2m18s
Some checks failed
Desktop Dev Pre-release / prerelease (push) Failing after 2m18s
- added dedicated GUI updater executable and integrated launch path from main app - added stable/beta update channel selection with persisted settings and checker support - expanded CI/release validation to include updater and full test discovery
This commit is contained in:
169
main.py
169
main.py
@@ -1,10 +1,22 @@
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import shutil
|
||||
import auth_webview
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
from PySide6.QtCore import QProcess
|
||||
from PySide6.QtCore import QStandardPaths
|
||||
from PySide6.QtCore import Qt, QUrl, QDateTime, QTimer
|
||||
from PySide6.QtGui import QIcon, QAction, QActionGroup, QDesktopServices
|
||||
from PySide6.QtWidgets import (QApplication, QMainWindow, QLabel, QLineEdit,
|
||||
QPushButton, QVBoxLayout, QWidget, QMessageBox,
|
||||
QTextBrowser, QScrollArea, QCheckBox, QHBoxLayout,
|
||||
QTabWidget, QDialog, QDialogButtonBox,
|
||||
QProgressBar)
|
||||
from vk_api.exceptions import VkApiError
|
||||
|
||||
import auth_webview
|
||||
from app_version import APP_VERSION
|
||||
from services import (
|
||||
AutoUpdateService,
|
||||
@@ -18,20 +30,14 @@ from services import (
|
||||
)
|
||||
from ui.dialogs import MultiLinkDialog as UIMultiLinkDialog
|
||||
from ui.main_window import instructions_text
|
||||
from PySide6.QtWidgets import (QApplication, QMainWindow, QLabel, QLineEdit,
|
||||
QPushButton, QVBoxLayout, QWidget, QMessageBox,
|
||||
QTextBrowser, QScrollArea, QCheckBox, QHBoxLayout,
|
||||
QSizePolicy, QTabWidget, QDialog, QDialogButtonBox,
|
||||
QProgressBar)
|
||||
from PySide6.QtCore import Qt, QUrl, QDateTime, QTimer
|
||||
from PySide6.QtGui import QIcon, QAction, QDesktopServices
|
||||
from vk_api.exceptions import VkApiError
|
||||
from PySide6.QtCore import QStandardPaths
|
||||
from PySide6.QtCore import QProcess
|
||||
|
||||
# --- Управление токенами и настройками ---
|
||||
APP_DATA_DIR = os.path.join(QStandardPaths.writableLocation(QStandardPaths.AppDataLocation), "AnabasisVKChatManager")
|
||||
APP_DATA_DIR = os.path.join(
|
||||
QStandardPaths.writableLocation(QStandardPaths.StandardLocation.AppDataLocation),
|
||||
"AnabasisVKChatManager",
|
||||
)
|
||||
TOKEN_FILE = os.path.join(APP_DATA_DIR, "token.json")
|
||||
SETTINGS_FILE = os.path.join(APP_DATA_DIR, "settings.json")
|
||||
WEB_ENGINE_CACHE_DIR = os.path.join(APP_DATA_DIR, "web_engine_cache")
|
||||
CACHE_CLEANUP_MARKER = os.path.join(APP_DATA_DIR, "pending_cache_cleanup")
|
||||
LOG_FILE = os.path.join(APP_DATA_DIR, "app.log")
|
||||
@@ -42,6 +48,7 @@ AUTH_RELOGIN_BACKOFF_SECONDS = 5.0
|
||||
UPDATE_REPOSITORY = ""
|
||||
# Full repository URL is preferred (supports GitHub/Gitea).
|
||||
UPDATE_REPOSITORY_URL = "https://git.daemonlord.ru/benya/AnabasisChatRemove"
|
||||
UPDATE_CHANNEL_DEFAULT = "stable"
|
||||
UPDATE_REQUEST_TIMEOUT = 8
|
||||
|
||||
|
||||
@@ -79,6 +86,7 @@ class VkChatManager(QMainWindow):
|
||||
self._auth_relogin_in_progress = False
|
||||
self._last_auth_relogin_ts = 0.0
|
||||
self.update_repository_url = detect_update_repository_url(UPDATE_REPOSITORY_URL, UPDATE_REPOSITORY)
|
||||
self.update_channel = UPDATE_CHANNEL_DEFAULT
|
||||
self.update_checker = None
|
||||
self.update_thread = None
|
||||
self._update_check_silent = False
|
||||
@@ -90,6 +98,7 @@ class VkChatManager(QMainWindow):
|
||||
|
||||
self._cleanup_cache_if_needed()
|
||||
self._ensure_log_dir()
|
||||
self._load_settings()
|
||||
self.init_ui()
|
||||
self.load_saved_token_on_startup()
|
||||
self.setup_token_timer()
|
||||
@@ -114,7 +123,7 @@ class VkChatManager(QMainWindow):
|
||||
layout.addWidget(self.token_input)
|
||||
|
||||
self.token_timer_label = QLabel("Срок действия токена: Н/Д")
|
||||
self.token_timer_label.setAlignment(Qt.AlignRight)
|
||||
self.token_timer_label.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
layout.addWidget(self.token_timer_label)
|
||||
|
||||
self.auth_btn = QPushButton("Авторизоваться через VK")
|
||||
@@ -176,7 +185,7 @@ class VkChatManager(QMainWindow):
|
||||
self.add_user_btn.clicked.connect(self.add_user_to_chat)
|
||||
layout.addWidget(self.add_user_btn)
|
||||
self.status_label = QLabel("Статус: не авторизован")
|
||||
self.status_label.setAlignment(Qt.AlignCenter)
|
||||
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(self.status_label)
|
||||
|
||||
layout.addStretch(1)
|
||||
@@ -230,6 +239,26 @@ class VkChatManager(QMainWindow):
|
||||
tools_menu.addAction(check_updates_action)
|
||||
self.check_updates_action = check_updates_action
|
||||
|
||||
channel_menu = tools_menu.addMenu("Канал обновлений")
|
||||
self.update_channel_group = QActionGroup(self)
|
||||
self.update_channel_group.setExclusive(True)
|
||||
|
||||
stable_channel_action = QAction("Релизы (stable)", self)
|
||||
stable_channel_action.setCheckable(True)
|
||||
stable_channel_action.setChecked(self.update_channel == "stable")
|
||||
stable_channel_action.triggered.connect(lambda checked: checked and self.set_update_channel("stable"))
|
||||
channel_menu.addAction(stable_channel_action)
|
||||
self.update_channel_group.addAction(stable_channel_action)
|
||||
self.update_channel_stable_action = stable_channel_action
|
||||
|
||||
beta_channel_action = QAction("Бета (pre-release)", self)
|
||||
beta_channel_action.setCheckable(True)
|
||||
beta_channel_action.setChecked(self.update_channel == "beta")
|
||||
beta_channel_action.triggered.connect(lambda checked: checked and self.set_update_channel("beta"))
|
||||
channel_menu.addAction(beta_channel_action)
|
||||
self.update_channel_group.addAction(beta_channel_action)
|
||||
self.update_channel_beta_action = beta_channel_action
|
||||
|
||||
logout_action = QAction("Выйти и очистить", self)
|
||||
logout_action.setStatusTip("Выйти, удалить токен и кэш")
|
||||
logout_action.triggered.connect(self.logout_and_clear)
|
||||
@@ -259,12 +288,12 @@ class VkChatManager(QMainWindow):
|
||||
"Поддерживается проверка обновлений и автообновление Windows-сборки.<br><br>"
|
||||
f"Репозиторий: {repo_html}"
|
||||
)
|
||||
content.setTextFormat(Qt.RichText)
|
||||
content.setTextInteractionFlags(Qt.TextBrowserInteraction)
|
||||
content.setTextFormat(Qt.TextFormat.RichText)
|
||||
content.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
|
||||
content.setOpenExternalLinks(True)
|
||||
content.setWordWrap(True)
|
||||
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.Ok, parent=dialog)
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok, parent=dialog)
|
||||
button_box.accepted.connect(dialog.accept)
|
||||
|
||||
layout = QVBoxLayout(dialog)
|
||||
@@ -281,8 +310,8 @@ class VkChatManager(QMainWindow):
|
||||
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True)
|
||||
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
|
||||
tab_layout.addWidget(scroll_area)
|
||||
|
||||
@@ -300,15 +329,60 @@ class VkChatManager(QMainWindow):
|
||||
if hasattr(self, "check_updates_action"):
|
||||
self.check_updates_action.setEnabled(not in_progress)
|
||||
|
||||
def _normalize_update_channel(self, value):
|
||||
channel = (value or "").strip().lower()
|
||||
if channel in ("beta", "betas", "pre", "prerelease", "pre-release"):
|
||||
return "beta"
|
||||
return "stable"
|
||||
|
||||
def _load_settings(self):
|
||||
self.update_channel = UPDATE_CHANNEL_DEFAULT
|
||||
if not os.path.exists(SETTINGS_FILE):
|
||||
return
|
||||
try:
|
||||
with open(SETTINGS_FILE, "r", encoding="utf-8") as f:
|
||||
settings = json.load(f)
|
||||
self.update_channel = self._normalize_update_channel(settings.get("update_channel"))
|
||||
except Exception as e:
|
||||
self._log_event("settings_load", f"Ошибка загрузки настроек: {e}", level="WARN")
|
||||
|
||||
def _save_settings(self):
|
||||
try:
|
||||
os.makedirs(APP_DATA_DIR, exist_ok=True)
|
||||
settings = {
|
||||
"update_channel": self.update_channel,
|
||||
}
|
||||
with open(SETTINGS_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(settings, f, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
self._log_event("settings_save", f"Ошибка сохранения настроек: {e}", level="WARN")
|
||||
|
||||
def set_update_channel(self, channel):
|
||||
normalized = self._normalize_update_channel(channel)
|
||||
if normalized == self.update_channel:
|
||||
return
|
||||
self.update_channel = normalized
|
||||
self._save_settings()
|
||||
self.status_label.setText(
|
||||
f"Статус: канал обновлений переключен на {'бета' if normalized == 'beta' else 'релизы'}."
|
||||
)
|
||||
self._log_event("update_channel", f"update_channel={self.update_channel}")
|
||||
|
||||
def check_for_updates(self, silent_no_updates=False):
|
||||
if self.update_thread and self.update_thread.is_alive():
|
||||
return
|
||||
|
||||
self._update_check_silent = silent_no_updates
|
||||
self._set_update_action_state(True)
|
||||
self.status_label.setText("Статус: проверка обновлений...")
|
||||
channel_label = "бета" if self.update_channel == "beta" else "релизы"
|
||||
self.status_label.setText(f"Статус: проверка обновлений ({channel_label})...")
|
||||
|
||||
self.update_checker = UpdateChecker(self.update_repository_url, APP_VERSION, request_timeout=UPDATE_REQUEST_TIMEOUT)
|
||||
self.update_checker = UpdateChecker(
|
||||
self.update_repository_url,
|
||||
APP_VERSION,
|
||||
request_timeout=UPDATE_REQUEST_TIMEOUT,
|
||||
channel=self.update_channel,
|
||||
)
|
||||
self.update_checker.check_finished.connect(self._on_update_check_finished)
|
||||
self.update_checker.check_failed.connect(self._on_update_check_failed)
|
||||
self.update_thread = threading.Thread(target=self.update_checker.run, daemon=True)
|
||||
@@ -324,17 +398,17 @@ class VkChatManager(QMainWindow):
|
||||
self.status_label.setText(f"Статус: доступно обновление {latest_version}")
|
||||
|
||||
message_box = QMessageBox(self)
|
||||
message_box.setIcon(QMessageBox.Information)
|
||||
message_box.setIcon(QMessageBox.Icon.Information)
|
||||
message_box.setWindowTitle("Доступно обновление")
|
||||
message_box.setText(
|
||||
f"Текущая версия: {result.get('current_version')}\n"
|
||||
f"Доступная версия: {latest_version}\n\n"
|
||||
"Открыть страницу загрузки?"
|
||||
)
|
||||
update_now_button = message_box.addButton("Обновить сейчас", QMessageBox.AcceptRole)
|
||||
download_button = message_box.addButton("Скачать", QMessageBox.AcceptRole)
|
||||
releases_button = message_box.addButton("Релизы", QMessageBox.ActionRole)
|
||||
cancel_button = message_box.addButton("Позже", QMessageBox.RejectRole)
|
||||
update_now_button = message_box.addButton("Обновить сейчас", QMessageBox.ButtonRole.AcceptRole)
|
||||
download_button = message_box.addButton("Скачать", QMessageBox.ButtonRole.AcceptRole)
|
||||
releases_button = message_box.addButton("Релизы", QMessageBox.ButtonRole.ActionRole)
|
||||
cancel_button = message_box.addButton("Позже", QMessageBox.ButtonRole.RejectRole)
|
||||
message_box.setDefaultButton(update_now_button)
|
||||
message_box.exec()
|
||||
|
||||
@@ -356,9 +430,10 @@ class VkChatManager(QMainWindow):
|
||||
self._log_event("update_check", "Диалог обновления закрыт без действия.", level="WARN")
|
||||
return
|
||||
|
||||
self.status_label.setText(f"Статус: используется актуальная версия ({APP_VERSION}).")
|
||||
channel_label = "бета" if self.update_channel == "beta" else "релизы"
|
||||
self.status_label.setText(f"Статус: используется актуальная версия ({APP_VERSION}, канал: {channel_label}).")
|
||||
if not self._update_check_silent:
|
||||
QMessageBox.information(self, "Обновления", "Установлена актуальная версия.")
|
||||
QMessageBox.information(self, "Обновления", f"Установлена актуальная версия в канале {channel_label}.")
|
||||
|
||||
def _on_update_check_failed(self, error_text):
|
||||
self._set_update_action_state(False)
|
||||
@@ -453,7 +528,7 @@ class VkChatManager(QMainWindow):
|
||||
self.status_label.setText(status_text)
|
||||
if busy:
|
||||
self._busy = True
|
||||
QApplication.setOverrideCursor(Qt.WaitCursor)
|
||||
QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
|
||||
for widget in [
|
||||
self.refresh_chats_btn, self.select_all_btn, self.deselect_all_btn,
|
||||
self.vk_url_input, self.multi_link_btn, self.visible_messages_checkbox,
|
||||
@@ -537,9 +612,9 @@ class VkChatManager(QMainWindow):
|
||||
self,
|
||||
"Подтверждение выхода",
|
||||
"Вы уверены, что хотите выйти и удалить сохраненный токен и кэш?",
|
||||
QMessageBox.Yes | QMessageBox.No
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
if confirm != QMessageBox.Yes:
|
||||
if confirm != QMessageBox.StandardButton.Yes:
|
||||
return
|
||||
|
||||
self._clear_auth_state(stop_timer=True, remove_token_file=True)
|
||||
@@ -596,7 +671,7 @@ class VkChatManager(QMainWindow):
|
||||
def _on_auth_process_error(self, process_error):
|
||||
self._auth_process_error_text = f"Ошибка запуска авторизации: {process_error}"
|
||||
# For failed starts Qt may not emit finished(), so release UI here.
|
||||
if self.auth_process and self.auth_process.state() == QProcess.NotRunning:
|
||||
if self.auth_process and self.auth_process.state() == QProcess.ProcessState.NotRunning:
|
||||
output_path = self.auth_output_path
|
||||
self.auth_output_path = None
|
||||
self.auth_process = None
|
||||
@@ -656,7 +731,7 @@ class VkChatManager(QMainWindow):
|
||||
self.handle_new_auth_token(token, expires_in)
|
||||
|
||||
def start_auth(self, keep_status_text=False):
|
||||
if self.auth_process and self.auth_process.state() != QProcess.NotRunning:
|
||||
if self.auth_process and self.auth_process.state() != QProcess.ProcessState.NotRunning:
|
||||
self._log_event("auth_process", "Авторизация уже запущена, повторный запуск пропущен.")
|
||||
return
|
||||
|
||||
@@ -880,9 +955,9 @@ class VkChatManager(QMainWindow):
|
||||
confirm_dialog = QMessageBox(self)
|
||||
confirm_dialog.setWindowTitle("Подтверждение действия")
|
||||
confirm_dialog.setText(msg)
|
||||
confirm_dialog.setIcon(QMessageBox.Question)
|
||||
yes_button = confirm_dialog.addButton("Да", QMessageBox.YesRole)
|
||||
no_button = confirm_dialog.addButton("Нет", QMessageBox.NoRole)
|
||||
confirm_dialog.setIcon(QMessageBox.Icon.Question)
|
||||
yes_button = confirm_dialog.addButton("Да", QMessageBox.ButtonRole.YesRole)
|
||||
no_button = confirm_dialog.addButton("Нет", QMessageBox.ButtonRole.NoRole)
|
||||
confirm_dialog.setDefaultButton(no_button) # "Нет" - кнопка по умолчанию
|
||||
|
||||
confirm_dialog.exec()
|
||||
@@ -955,9 +1030,9 @@ class VkChatManager(QMainWindow):
|
||||
confirm_dialog = QMessageBox(self)
|
||||
confirm_dialog.setWindowTitle("Подтверждение прав")
|
||||
confirm_dialog.setText(msg)
|
||||
confirm_dialog.setIcon(QMessageBox.Question)
|
||||
yes_button = confirm_dialog.addButton("Да", QMessageBox.YesRole)
|
||||
no_button = confirm_dialog.addButton("Нет", QMessageBox.NoRole)
|
||||
confirm_dialog.setIcon(QMessageBox.Icon.Question)
|
||||
yes_button = confirm_dialog.addButton("Да", QMessageBox.ButtonRole.YesRole)
|
||||
no_button = confirm_dialog.addButton("Нет", QMessageBox.ButtonRole.NoRole)
|
||||
confirm_dialog.setDefaultButton(no_button)
|
||||
|
||||
confirm_dialog.exec()
|
||||
@@ -1129,18 +1204,18 @@ class VkChatManager(QMainWindow):
|
||||
download_name=download_name,
|
||||
)
|
||||
app_exe = sys.executable
|
||||
script_path = AutoUpdateService.build_update_script(
|
||||
app_dir=os.path.dirname(app_exe),
|
||||
AutoUpdateService.launch_gui_updater(
|
||||
app_exe=app_exe,
|
||||
source_dir=source_dir,
|
||||
exe_name=os.path.basename(app_exe),
|
||||
work_dir=work_dir,
|
||||
target_pid=os.getpid(),
|
||||
version=latest_version,
|
||||
)
|
||||
AutoUpdateService.launch_update_script(script_path, work_dir)
|
||||
self._log_event("auto_update", f"Update {latest_version} started from {download_url}")
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Обновление запущено",
|
||||
"Обновление скачано. Приложение будет перезапущено.",
|
||||
"Обновление скачано. Открылось окно обновления.",
|
||||
)
|
||||
QApplication.instance().quit()
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user