#!/usr/bin/env python3 import os import subprocess import sys import shlex import shutil from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,QPushButton, QLabel, QTabWidget, QTextEdit, QFileDialog, QMessageBox, QLineEdit, QCheckBox, QStackedWidget, QScrollArea, QGridLayout, QFrame, QDialog, QTextBrowser) from PyQt5.QtCore import Qt, QProcess, QSize, QTimer, QProcessEnvironment from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter class Var: # Переменные определяемые в скрипте winehelper SCRIPT_NAME = os.environ.get("SCRIPT_NAME") USER_WORK_PATH = os.environ.get("USER_WORK_PATH") RUN_SCRIPT = os.environ.get("RUN_SCRIPT") DATA_PATH = os.environ.get("DATA_PATH") CHANGELOG_FILE = os.environ.get("CHANGELOG_FILE") WH_ICON_PATH = os.environ.get("WH_ICON_PATH") LICENSE_FILE = os.environ.get("LICENSE_FILE") class WineHelperGUI(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("WineHelper") self.setGeometry(100, 100, 950, 500) if Var.WH_ICON_PATH and os.path.exists(Var.WH_ICON_PATH): self.setWindowIcon(QIcon(Var.WH_ICON_PATH)) # Центрирование окна screen = QApplication.primaryScreen() screen_geometry = screen.availableGeometry() self.move( (screen_geometry.width() - self.width()) // 2, (screen_geometry.height() - self.height()) // 2 ) # Стиль для кнопок в списках self.BUTTON_LIST_STYLE = """ QPushButton { text-align: left; padding-left: 10px; padding-right: 10px; height: 42px; min-height: 42px; max-height: 42px; } QPushButton::icon { padding-left: 10px; } """ self.INSTALLED_BUTTON_LIST_STYLE = self.BUTTON_LIST_STYLE.replace( "padding-left: 10px;", "padding-left: 15px;" ) # Стили для оберток кнопок (для рамки выделения) self.FRAME_STYLE_DEFAULT = "QFrame { border: 2px solid transparent; border-radius: 2px; padding: 0px; }" self.FRAME_STYLE_SELECTED = "QFrame { border: 2px solid #0078d7; border-radius: 2px; padding: 0px; }" # Основные переменные self.winehelper_path = Var.RUN_SCRIPT self.process = None self.current_script = None self.install_process = None self.install_dialog = None self.current_active_button = None self.installed_buttons = [] self.current_selected_app = None self.icon_animators = {} # Создаем главный виджет и layout self.main_widget = QWidget() self.setCentralWidget(self.main_widget) self.main_layout = QHBoxLayout() self.main_widget.setLayout(self.main_layout) # Создаем табы self.tabs = QTabWidget() self.main_layout.addWidget(self.tabs, stretch=2) # Создаем панель информации о скрипте self.create_info_panel() # Вкладки self.create_auto_install_tab() self.create_manual_install_tab() self.create_installed_tab() self.create_help_tab() # Обновляем список установленных приложений self.update_installed_apps() # Соединяем сигнал смены вкладок с функцией self.tabs.currentChanged.connect(self.on_tab_changed) # Устанавливаем начальное состояние видимости панели self.on_tab_changed(self.tabs.currentIndex()) def _reset_info_panel_to_default(self, tab_name): """Сбрасывает правую панель в состояние по умолчанию для указанной вкладки.""" if tab_name == "Автоматическая установка": title = "Автоматическая установка" html_content = ("
Скрипты из этого списка скачают, установят и настроят приложение за вас.
" "Просто выберите программу и нажмите «Установить».
") show_global = False elif tab_name == "Ручная установка": title = "Ручная установка" html_content = ("Эти скрипты подготовят окружение для установки.
" "Вам нужно будет указать только путь к установочному файлу (.exe
или .msi
), который вы скачали самостоятельно и нажать «Установить».
Здесь отображаются все приложения, установленные с помощью WineHelper.
" "Выберите программу, чтобы увидеть доступные действия.
" "Также на этой вкладке можно восстановить префикс из резервной копии с помощью соответствующей кнопки.
") show_global = True else: return self.script_title.setText(title) self.script_description.setHtml(html_content) self.script_description.setVisible(True) self.script_title.setPixmap(QPixmap()) self.install_action_widget.setVisible(False) self.installed_action_widget.setVisible(False) self.installed_global_action_widget.setVisible(show_global) self.manual_install_path_widget.setVisible(False) if show_global: self.backup_button.setVisible(False) self.create_log_button.setVisible(False) self.current_selected_app = None def on_tab_changed(self, index): """Скрывает или показывает панель информации в зависимости от активной вкладки.""" current_tab_text = self.tabs.tabText(index) # Сбрасываем растяжение к состоянию по умолчанию: # растягивается виджет с описанием (индекс 1), а не виджет с действиями (индекс 4) self.info_panel_layout.setStretch(1, 1) self.info_panel_layout.setStretch(4, 0) if current_tab_text in ["Автоматическая установка", "Ручная установка", "Установленные"]: self.info_panel.setVisible(True) self._reset_info_panel_to_default(current_tab_text) if current_tab_text == "Установленные": self.filter_installed_buttons() else: self.info_panel.setVisible(False) def create_info_panel(self): """Создает правую панель с информацией о скрипте""" self.info_panel = QFrame() self.info_panel.setFrameShape(QFrame.StyledPanel) self.info_panel.setMinimumWidth(400) # Размер инф. панели self.info_panel.setFont(QFont('Arial', 10)) # Шрифт и размер шрифта в инф. панели self.info_panel_layout = QVBoxLayout() self.info_panel.setLayout(self.info_panel_layout) # Заголовок self.script_title = QLabel("Выберите программу") self.script_title.setFont(QFont('Arial', 14, QFont.Bold)) # Шрифт и размер шрифта в заголовке инф. панели self.script_title.setAlignment(Qt.AlignCenter) self.info_panel_layout.addWidget(self.script_title) # Описание self.script_description = QTextBrowser() self.script_description.setReadOnly(True) self.script_description.setOpenExternalLinks(True) self.info_panel_layout.addWidget(self.script_description) # Строка для ввода пути установочного файла self.manual_install_path_layout = QVBoxLayout() self.install_path_label = QLabel("Путь к установочному файлу:") self.manual_install_path_layout.addWidget(self.install_path_label) path_input_layout = QHBoxLayout() self.install_path_edit = QLineEdit() self.install_path_edit.setPlaceholderText("Укажите путь к установочному файлу...") self.browse_button = QPushButton("Обзор...") self.browse_button.clicked.connect(self.browse_install_file) path_input_layout.addWidget(self.install_path_edit) path_input_layout.addWidget(self.browse_button) self.manual_install_path_layout.addLayout(path_input_layout) self.manual_install_path_widget = QWidget() self.manual_install_path_widget.setLayout(self.manual_install_path_layout) self.manual_install_path_widget.setVisible(False) self.info_panel_layout.addWidget(self.manual_install_path_widget) # --- ВИДЖЕТЫ ДЛЯ ДЕЙСТВИЙ --- # Виджет для действий установщика self.install_action_widget = QWidget() install_action_layout = QVBoxLayout() install_action_layout.setContentsMargins(0, 0, 0, 0) self.install_button = QPushButton("Установить") self.install_button.setFont(QFont('Arial', 13, QFont.Bold)) self.install_button.setStyleSheet("background-color: #4CAF50; color: white;") self.install_button.clicked.connect(self.install_current_script) install_action_layout.addWidget(self.install_button) self.install_action_widget.setLayout(install_action_layout) self.info_panel_layout.addWidget(self.install_action_widget) # Виджет для действий с установленным приложением self.installed_action_widget = QWidget() installed_action_layout = QVBoxLayout() installed_action_layout.setContentsMargins(0, 0, 0, 0) installed_action_layout.setSpacing(5) top_buttons_layout = QHBoxLayout() self.run_button = QPushButton("Запустить") self.run_button.clicked.connect(self.run_installed_app) self.uninstall_button = QPushButton("Удалить префикс") self.uninstall_button.clicked.connect(self.uninstall_app) top_buttons_layout.addWidget(self.run_button) top_buttons_layout.addWidget(self.uninstall_button) installed_action_layout.addLayout(top_buttons_layout) self.installed_action_widget.setLayout(installed_action_layout) self.info_panel_layout.addWidget(self.installed_action_widget) self.installed_global_action_widget = QWidget() installed_global_layout = QVBoxLayout() installed_global_layout.setContentsMargins(0, 10, 0, 0) self.create_log_button = QPushButton("Создать лог запуска программы") self.create_log_button.setIcon(QIcon.fromTheme("view-list-text")) self.create_log_button.clicked.connect(self.run_installed_app_with_debug) installed_global_layout.addWidget(self.create_log_button) self.backup_button = QPushButton("Создать резервную копию префикса") self.backup_button.setIcon(QIcon.fromTheme("document-save")) self.backup_button.clicked.connect(self.backup_prefix_for_app) installed_global_layout.addWidget(self.backup_button) self.restore_prefix_button_panel = QPushButton("Восстановить префикс из резервной копии") self.restore_prefix_button_panel.setIcon(QIcon.fromTheme("document-revert")) self.restore_prefix_button_panel.clicked.connect(self.restore_prefix) installed_global_layout.addWidget(self.restore_prefix_button_panel) self.installed_global_action_widget.setLayout(installed_global_layout) self.info_panel_layout.addWidget(self.installed_global_action_widget) # Изначально скрыть все виджеты действий self.install_action_widget.setVisible(False) self.installed_action_widget.setVisible(False) self.installed_global_action_widget.setVisible(False) self.main_layout.addWidget(self.info_panel, stretch=1) def browse_install_file(self): """Открывает диалог выбора файла для ручной установки""" file_path, _ = QFileDialog.getOpenFileName( self, "Выберите установочный файл", os.path.expanduser("~"), "Все файлы (*);;Исполняемые файлы (*.exe *.msi)" ) if file_path: self.install_path_edit.setText(file_path) def extract_icons_from_script(self, script_path): """ Извлекает иконку для скрипта. Сначала ищет переменную 'export PROG_ICON=', если не находит, то ищет все вызовы 'create_desktop' и берет иконки из третьего аргумента. Возвращает список имен иконок. """ try: with open(script_path, 'r', encoding='utf-8') as f: lines = f.readlines() # 1. Приоритет у PROG_ICON for line in lines: if line.strip().startswith('export PROG_ICON='): icon_name = line.split('=', 1)[1].strip().strip('"\'') if icon_name: return [icon_name] # 2. Если PROG_ICON не найден, ищем все вызовы create_desktop icon_names = [] for line in lines: line = line.strip() # Пропускаем закомментированные строки и пустые строки if not line or line.startswith('#'): continue if 'create_desktop' in line: try: parts = shlex.split(line) # Ищем все вхождения, а не только первое for i, part in enumerate(parts): if part == 'create_desktop': if len(parts) > i + 3: icon_name = parts[i + 3] if icon_name: icon_names.append(icon_name) except (ValueError, IndexError): continue return icon_names except Exception as e: print(f"Ошибка чтения файла для извлечения иконки: {str(e)}") return [] def extract_prog_name_from_script(self, script_path): """Извлекает имя программы из строки PROG_NAME= в скрипте""" try: with open(script_path, 'r', encoding='utf-8') as f: for line in f: # Ищем строку, которая начинается с PROG_NAME= или export PROG_NAME= if line.strip().startswith(('export PROG_NAME=', 'PROG_NAME=')): # Отделяем имя переменной от значения и убираем кавычки name = line.split('=', 1)[1].strip().strip('"\'') if name: return name return None except Exception as e: print(f"Ошибка чтения файла для извлечения PROG_NAME: {str(e)}") return None def extract_prog_url_from_script(self, script_path): """Извлекает URL из строки export PROG_URL= в скрипте""" try: with open(script_path, 'r', encoding='utf-8') as f: for line in f: if line.startswith('export PROG_URL='): return line.replace('export PROG_URL=', '').strip().strip('"\'') return None except Exception as e: print(f"Ошибка чтения файла для извлечения PROG_URL: {str(e)}") return None def _ease_in_out_quad(self, t): """Простая квадратичная функция сглаживания (ease-in-out).""" # t - значение от 0.0 до 1.0 if t < 0.5: return 2 * t * t return 1 - pow(-2 * t + 2, 2) / 2 def _start_icon_fade_animation(self, button): """Запускает анимацию плавного перехода для иконки на кнопке.""" if button not in self.icon_animators: return anim_data = self.icon_animators[button] # Останавливаем предыдущую анимацию, если она еще идет if anim_data.get('fade_timer') and anim_data['fade_timer'].isActive(): anim_data['fade_timer'].stop() # Определяем текущую и следующую иконки current_icon_path = anim_data['icons'][anim_data['current_index']] next_icon_index = (anim_data['current_index'] + 1) % len(anim_data['icons']) next_icon_path = anim_data['icons'][next_icon_index] current_pixmap = QPixmap(current_icon_path) next_pixmap = QPixmap(next_icon_path) # Запускаем таймер для покадровой анимации anim_data['fade_step'] = 0 fade_timer = QTimer(button) anim_data['fade_timer'] = fade_timer fade_timer.timeout.connect( lambda b=button, p1=current_pixmap, p2=next_pixmap: self._update_fade_frame(b, p1, p2) ) fade_timer.start(16) # ~60 кадров в секунду def _update_fade_frame(self, button, old_pixmap, new_pixmap): """Обновляет кадр анимации перехода иконок.""" if button not in self.icon_animators: return anim_data = self.icon_animators[button] # Длительность анимации: 44 шага * 16 мс ~= 700 мс FADE_DURATION_STEPS = 44 anim_data['fade_step'] += 1 progress_linear = anim_data['fade_step'] / FADE_DURATION_STEPS # Применяем функцию сглаживания для более плавного старта и завершения progress = self._ease_in_out_quad(progress_linear) if progress_linear >= 1.0: anim_data['fade_timer'].stop() anim_data['current_index'] = (anim_data['current_index'] + 1) % len(anim_data['icons']) button.setIcon(QIcon(new_pixmap)) return canvas = QPixmap(button.iconSize()) canvas.fill(Qt.transparent) painter = QPainter(canvas) painter.setRenderHint(QPainter.Antialiasing, True) painter.setRenderHint(QPainter.SmoothPixmapTransform, True) painter.setOpacity(1.0 - progress) painter.drawPixmap(canvas.rect(), old_pixmap) painter.setOpacity(progress) painter.drawPixmap(canvas.rect(), new_pixmap) painter.end() button.setIcon(QIcon(canvas)) def _create_app_button(self, text, icon_paths, style_sheet): """Создает и настраивает стандартную кнопку для списков приложений.""" # Добавляем пробелы перед текстом для создания отступа от иконки btn = QPushButton(" " + text) btn.setStyleSheet(style_sheet) # Отфильтровываем несуществующие пути existing_icon_paths = [path for path in icon_paths if path and os.path.exists(path)] if existing_icon_paths: if len(existing_icon_paths) > 1: # Устанавливаем первую иконку и запускаем анимацию btn.setIcon(QIcon(existing_icon_paths[0])) main_timer = QTimer(btn) self.icon_animators[btn] = {'main_timer': main_timer, 'icons': existing_icon_paths, 'current_index': 0} main_timer.timeout.connect(lambda b=btn: self._start_icon_fade_animation(b)) main_timer.start(2500) # Интервал между сменами иконок else: # Устанавливаем одну иконку btn.setIcon(QIcon(existing_icon_paths[0])) btn.setIconSize(QSize(32, 32)) else: # Устанавливаем пустую иконку, если ничего не найдено, для сохранения отступов btn.setIcon(QIcon()) btn.setIconSize(QSize(32, 32)) return btn def _populate_install_grid(self, grid_layout, scripts_list, script_folder, button_list): """ Заполняет QGridLayout кнопками установщиков. Кнопки создаются только для скриптов, в которых найдена переменная PROG_NAME. :param grid_layout: QGridLayout для заполнения. :param scripts_list: Список имен скриптов. :param script_folder: Имя папки со скриптами ('autoinstall' или 'manualinstall'). :param button_list: Список для хранения созданных кнопок. """ button_index = 0 for script in scripts_list: script_path = os.path.join(Var.DATA_PATH, script_folder, script) prog_name = self.extract_prog_name_from_script(script_path) # Создаем кнопку, только если для скрипта указано имя программы if not prog_name: continue icon_names = self.extract_icons_from_script(script_path) icon_paths = [os.path.join(Var.DATA_PATH, "image", f"{name}.png") for name in icon_names] btn = self._create_app_button(prog_name, icon_paths, self.BUTTON_LIST_STYLE) # Обертка для рамки выделения frame = QFrame() frame.setStyleSheet(self.FRAME_STYLE_DEFAULT) layout = QVBoxLayout(frame) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(btn) btn.clicked.connect(lambda _, s=script, b=btn: self.show_script_info(s, b)) row, column = divmod(button_index, 2) grid_layout.addWidget(frame, row, column) button_list.append(btn) button_index += 1 def _create_searchable_grid_tab(self, placeholder_text, filter_slot): """ Создает стандартную вкладку с полем поиска и сеточным макетом с прокруткой. Возвращает кортеж (главный виджет вкладки, сеточный макет, поле поиска, область прокрутки). """ tab_widget = QWidget() layout = QVBoxLayout(tab_widget) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(5) search_edit = QLineEdit() search_edit.setPlaceholderText(placeholder_text) search_edit.textChanged.connect(filter_slot) layout.addWidget(search_edit) scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setContentsMargins(0, 0, 0, 0) layout.addWidget(scroll_area) scroll_content_widget = QWidget() scroll_area.setWidget(scroll_content_widget) v_scroll_layout = QVBoxLayout(scroll_content_widget) v_scroll_layout.setContentsMargins(0, 0, 0, 0) v_scroll_layout.setSpacing(0) grid_layout = QGridLayout() grid_layout.setContentsMargins(5, 5, 5, 5) grid_layout.setSpacing(2) grid_layout.setColumnStretch(0, 1) grid_layout.setColumnStretch(1, 1) v_scroll_layout.addLayout(grid_layout) v_scroll_layout.addStretch(1) return tab_widget, grid_layout, search_edit, scroll_area def _create_and_populate_install_tab(self, tab_title, script_folder, search_placeholder, filter_slot): """ Создает и заполняет вкладку для установки (автоматической или ручной). Возвращает кортеж со скриптами, кнопками и виджетами. """ tab_widget, grid_layout, search_edit, scroll_area = self._create_searchable_grid_tab( search_placeholder, filter_slot ) scripts = [] script_path = os.path.join(Var.DATA_PATH, script_folder) if os.path.isdir(script_path): try: scripts = sorted(os.listdir(script_path)) except OSError as e: print(f"Не удалось прочитать директорию {script_path}: {e}") buttons_list = [] self._populate_install_grid(grid_layout, scripts, script_folder, buttons_list) self.tabs.addTab(tab_widget, tab_title) return scripts, buttons_list, grid_layout, search_edit, scroll_area def create_auto_install_tab(self): """Создает вкладку для автоматической установки программ""" ( self.autoinstall_scripts, self.autoinstall_buttons, self.scroll_layout, self.search_edit, self.auto_scroll_area ) = self._create_and_populate_install_tab( "Автоматическая установка", "autoinstall", "Поиск скрипта автоматической установки...", self.filter_autoinstall_buttons ) def create_manual_install_tab(self): """Создает вкладку для ручной установки программ""" ( self.manualinstall_scripts, self.manualinstall_buttons, self.manual_scroll_layout, self.manual_search_edit, self.manual_scroll_area ) = self._create_and_populate_install_tab( "Ручная установка", "manualinstall", "Поиск скрипта ручной установки...", self.filter_manual_buttons ) def create_installed_tab(self): """Создает вкладку для отображения установленных программ в виде кнопок""" installed_tab = QWidget() installed_layout = QVBoxLayout() installed_layout.setContentsMargins(0, 0, 0, 0) installed_layout.setSpacing(5) installed_tab.setLayout(installed_layout) installed_tab, self.installed_scroll_layout, self.installed_search_edit, self.installed_scroll_area = self._create_searchable_grid_tab( "Поиск установленной программы...", self.filter_installed_buttons ) self.tabs.addTab(installed_tab, "Установленные") def create_help_tab(self): """Создает вкладку 'Справка' с подвкладками""" help_tab = QWidget() help_layout = QVBoxLayout(help_tab) help_layout.setContentsMargins(5, 5, 5, 5) help_subtabs = QTabWidget() help_layout.addWidget(help_subtabs) # Подвкладка "Руководство" guide_tab = QWidget() guide_layout = QVBoxLayout(guide_tab) guide_text = QTextBrowser() guide_text.setOpenExternalLinks(True) guide_text.setHtml("""Подробное и актуальное руководство по использованию WineHelper смотрите на https://www.altlinux.org/Winehelper
""") guide_layout.addWidget(guide_text) help_subtabs.addTab(guide_tab, "Руководство") # Подвкладка "Авторы" authors_tab = QWidget() authors_layout = QVBoxLayout(authors_tab) authors_text = QTextEdit() authors_text.setReadOnly(True) authors_text.setHtml("""Помощники
Иван Мажукин (vanomj)
Идея и поддержка:
сообщество ALT Linux
Отдельная благодарность всем, кто вносит свой вклад в развитие проекта,
тестирует и сообщает об ошибках!
{escaped_license_content}
Некоторые компоненты, используемые или устанавливаемые данным ПО, могут иметь собственные лицензии. Пользователь несет полную ответственность за соблюдение этих лицензионных соглашений.
Ниже приведен список основных сторонних компонентов и ссылки на их исходный код:
""" # Читаем и парсим файл THIRD-PARTY third_party_html = "" third_party_file_path = os.path.join(Var.DATA_PATH, "THIRD-PARTY") if os.path.exists(third_party_file_path): with open(third_party_file_path, 'r', encoding='utf-8') as f_tp: third_party_content = f_tp.read() # Преобразуем контент в HTML third_party_html += '' for line in third_party_content.splitlines(): line = line.strip() if not line: third_party_html += '' license_text.setHtml(license_html + third_party_html) except (FileNotFoundError, TypeError): license_text.setHtml(f'
' continue escaped_line = html.escape(line) if line.startswith('http'): third_party_html += f' {escaped_line}
' else: third_party_html += f'{escaped_line}
' third_party_html += '
Не удалось загрузить файл лицензии по пути:
{Var.LICENSE_FILE}
Произошла ошибка при чтении файла лицензии:
{str(e)}
{description}
" if prog_url: html_description += f'Официальный сайт: {prog_url}
' self.script_description.setHtml(html_description) self.script_description.setVisible(True) self.install_action_widget.setVisible(True) self.installed_action_widget.setVisible(False) self.installed_global_action_widget.setVisible(False) self.install_button.setText(f"Установить {display_name}") def install_current_script(self): """Устанавливает текущий выбранный скрипт""" if not self.current_script: QMessageBox.warning(self, "Ошибка", "Не выбрана программа для установки") return if self.current_script in self.manualinstall_scripts and not self.install_path_edit.text().strip(): QMessageBox.warning(self, "Ошибка", "Укажите путь к установочному файлу") return # Создаем диалоговое окно установки self.install_dialog = QDialog(self) self.install_dialog.setWindowTitle(f"Установка {self.current_script}") self.install_dialog.setMinimumSize(750, 400) self.install_dialog.setWindowModality(Qt.WindowModal) self.stacked_widget = QStackedWidget() layout = QVBoxLayout() layout.addWidget(self.stacked_widget) self.install_dialog.setLayout(layout) # Первая страница - лицензионное соглашение license_page = QWidget() license_layout = QVBoxLayout(license_page) license_text = QTextEdit() license_text.setReadOnly(True) # Получаем текст лицензии из скрипта winehelper script_path = os.path.join(Var.DATA_PATH, "winehelper") license_content = "" try: with open(script_path, 'r', encoding='utf-8') as f: capturing = False for line in f: if 'print_warning "Лицензионные соглашения использования сторонних компонентов:' in line: capturing = True continue if capturing: if 'Подтверждая продолжение установки' in line: break # Очищаем строку от лишних символов clean_line = line.strip() clean_line = clean_line.replace('print_warning "', '').replace('\\n', '\n') clean_line = clean_line.rstrip('"') license_content += clean_line + '\n' license_text.setHtml(f"""{license_content}
""") except Exception as e: print(f"Ошибка чтения файла для извлечения лицензии: {str(e)}") license_text.setHtml("""Не удалось загрузить текст лицензионного соглашения.
""") license_layout.addWidget(license_text) self.license_checkbox = QCheckBox("Я принимаю условия лицензионного соглашения") license_layout.addWidget(self.license_checkbox) self.btn_continue = QPushButton("Продолжить установку") self.btn_continue.setEnabled(False) self.btn_continue.clicked.connect(self._prepare_installation) license_layout.addWidget(self.btn_continue) # Вторая страница - лог установки log_page = QWidget() log_layout = QVBoxLayout(log_page) self.log_output = QTextEdit() self.log_output.setReadOnly(True) self.log_output.setFont(QFont('DejaVu Sans Mono', 10)) log_layout.addWidget(self.log_output) self.control_buttons = QWidget() btn_layout = QHBoxLayout(self.control_buttons) self.btn_abort = QPushButton("Прервать") self.btn_abort.clicked.connect(self.abort_installation) btn_layout.addWidget(self.btn_abort) self.btn_close = QPushButton("Закрыть") self.btn_close.setEnabled(False) self.btn_close.clicked.connect(self.install_dialog.close) btn_layout.addWidget(self.btn_close) log_layout.addWidget(self.control_buttons) self.stacked_widget.addWidget(license_page) self.stacked_widget.addWidget(log_page) # Назначение кастомного обработчика закрытия окна def dialog_close_handler(event): self.handle_install_dialog_close(event) self.install_dialog.closeEvent = dialog_close_handler self.license_checkbox.stateChanged.connect( lambda state: self.btn_continue.setEnabled(state == Qt.Checked) ) self.install_dialog.show() def _prepare_installation(self): """Подготавливает и запускает процесс установки""" self.stacked_widget.setCurrentIndex(1) winehelper_path = self.winehelper_path script_path = os.path.join(Var.DATA_PATH, "autoinstall" if self.current_script in self.autoinstall_scripts else "manualinstall", self.current_script) if not os.path.exists(winehelper_path): QMessageBox.critical(self.install_dialog, "Ошибка", f"winehelper не найден по пути:\n{winehelper_path}") return if not os.path.exists(script_path): QMessageBox.critical(self.install_dialog, "Ошибка", f"Скрипт установки не найден:\n{script_path}") return if self.current_script in self.manualinstall_scripts: install_file = self.install_path_edit.text().strip() if not install_file: QMessageBox.critical(self.install_dialog, "Ошибка", "Не указан путь к установочному файлу") return QTimer.singleShot(100, lambda: self._start_installation(winehelper_path, script_path, install_file)) else: QTimer.singleShot(100, lambda: self._start_installation(winehelper_path, script_path)) def _start_installation(self, winehelper_path, script_path, install_file=None): """Запускает процесс установки""" self.install_process = QProcess() self.install_process.setProcessChannelMode(QProcess.MergedChannels) self.install_process.setWorkingDirectory(os.path.dirname(winehelper_path)) env = QProcessEnvironment.systemEnvironment() env.insert("GUI_MODE", "1") self.install_process.setProcessEnvironment(env) self.install_process.readyReadStandardOutput.connect(self.handle_process_output) self.install_process.finished.connect(self.handle_process_finished) args = ["install", os.path.basename(script_path)] if install_file: args.append(install_file) self.append_log(f"=== Начало установки {self.current_script} ===") self.append_log(f"Исполняемый файл: {winehelper_path}") self.append_log(f"Аргументы: {' '.join(shlex.quote(a) for a in args)}") try: self.install_process.start(winehelper_path, args) if not self.install_process.waitForStarted(3000): raise RuntimeError("Не удалось запустить процесс установки") self.append_log("Процесс установки успешно запущен...") except Exception as e: self.append_log(f"\n=== ОШИБКА: {str(e)} ===", is_error=True) QMessageBox.critical(self.install_dialog, "Ошибка", f"Не удалось запустить установку:\n{str(e)}") self.cleanup_process() def append_log(self, text, is_error=False): """Добавляет сообщение в лог""" if not hasattr(self, 'log_output'): return cursor = self.log_output.textCursor() cursor.movePosition(QTextCursor.End) if is_error: cursor.insertHtml(f'{text}