diff --git a/winehelper_gui.py b/winehelper_gui.py new file mode 100644 index 0000000..2d9132c --- /dev/null +++ b/winehelper_gui.py @@ -0,0 +1,1530 @@ +#!/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}