forked from CastroFidel/winehelper
		
	added a prefix creation tab
This commit is contained in:
		| @@ -11,10 +11,10 @@ import json | ||||
| import hashlib | ||||
| from functools import partial | ||||
| from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,QPushButton, QLabel, QTabWidget, | ||||
|                              QTextEdit, QFileDialog, QMessageBox, QLineEdit, QCheckBox, QStackedWidget, QScrollArea, | ||||
|                              QTextEdit, QFileDialog, QMessageBox, QLineEdit, QCheckBox, QStackedWidget, QScrollArea, QFormLayout, QGroupBox, QRadioButton, QComboBox, | ||||
|                              QListWidget, QListWidgetItem, QGridLayout, QFrame, QDialog, QTextBrowser) | ||||
| from PyQt5.QtCore import Qt, QProcess, QSize, QTimer, QProcessEnvironment, QPropertyAnimation, QEasingCurve | ||||
| from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter | ||||
| from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter, QDesktopServices | ||||
| from PyQt5.QtNetwork import QLocalServer, QLocalSocket | ||||
|  | ||||
|  | ||||
| @@ -1011,6 +1011,205 @@ class ScriptParser: | ||||
|         except Exception as e: | ||||
|             return f"Ошибка чтения файла: {str(e)}" | ||||
|  | ||||
| class WineVersionSelectionDialog(QDialog): | ||||
|     """Диалог для выбора версии Wine/Proton с группировкой.""" | ||||
|  | ||||
|     def __init__(self, architecture, winehelper_path, user_work_path, parent=None): | ||||
|         super().__init__(parent) | ||||
|         self.architecture = architecture | ||||
|         self.winehelper_path = winehelper_path | ||||
|         self.user_work_path = user_work_path | ||||
|         self.selected_version = None | ||||
|         self.wine_versions_data = {} | ||||
|  | ||||
|         self.setWindowTitle(f"Выбор версии Wine/Proton для {architecture} префикса") | ||||
|         self.setMinimumSize(900, 500) | ||||
|         self.setModal(True) | ||||
|  | ||||
|         main_layout = QVBoxLayout(self) | ||||
|  | ||||
|         self.search_edit = QLineEdit() | ||||
|         self.search_edit.setPlaceholderText("Поиск версии...") | ||||
|         self.search_edit.textChanged.connect(self.filter_versions) | ||||
|         main_layout.addWidget(self.search_edit) | ||||
|  | ||||
|         self.version_tabs = QTabWidget() | ||||
|         main_layout.addWidget(self.version_tabs) | ||||
|         button_layout = QHBoxLayout() | ||||
|         self.refresh_button = QPushButton("Обновить список") | ||||
|         self.refresh_button.setIcon(QIcon.fromTheme("view-refresh")) | ||||
|         self.refresh_button.clicked.connect(self.load_versions) | ||||
|         button_layout.addStretch() | ||||
|         button_layout.addWidget(self.refresh_button) | ||||
|         main_layout.addLayout(button_layout) | ||||
|  | ||||
|         self.load_versions() | ||||
|  | ||||
|     def load_versions(self): | ||||
|         """Запускает процесс получения списка версий Wine.""" | ||||
|         if not shutil.which('jq'): | ||||
|             QMessageBox.critical(self, "Ошибка", "Утилита 'jq' не найдена. Невозможно получить список версий Wine.\n\nУстановите пакет 'jq'.")             | ||||
|             return | ||||
|  | ||||
|         self.version_tabs.clear() | ||||
|         loading_widget = QWidget() | ||||
|         loading_layout = QVBoxLayout(loading_widget) | ||||
|         status_label = QLabel("Загрузка, пожалуйста, подождите...") | ||||
|         status_label.setAlignment(Qt.AlignCenter) | ||||
|         loading_layout.addWidget(status_label) | ||||
|         self.version_tabs.addTab(loading_widget, "Загрузка...") | ||||
|         self.version_tabs.setEnabled(False) | ||||
|  | ||||
|         self.refresh_button.setEnabled(False) | ||||
|         self.db_process = QProcess(self) | ||||
|         self.db_process.setProcessChannelMode(QProcess.MergedChannels) | ||||
|         self.db_process.finished.connect(self._on_db_generation_finished) | ||||
|         self.db_process.start(self.winehelper_path, ["generate-db"]) | ||||
|  | ||||
|     def _on_db_generation_finished(self, exit_code, exit_status): | ||||
|         """Обрабатывает завершение генерации метаданных Wine.""" | ||||
|         self.refresh_button.setEnabled(True) | ||||
|         self.version_tabs.setEnabled(True) | ||||
|  | ||||
|         error_message = None | ||||
|         if exit_code != 0: | ||||
|             error_output = self.db_process.readAll().data().decode('utf-8', 'ignore') | ||||
|             QMessageBox.warning(self, "Ошибка", f"Не удалось получить список версий Wine.\n\n{error_output}") | ||||
|             error_message = "Ошибка загрузки списка версий." | ||||
|         else: | ||||
|             metadata_file = os.path.join(self.user_work_path, "tmp", "wine_metadata.json") | ||||
|             if not os.path.exists(metadata_file): | ||||
|                 QMessageBox.warning(self, "Ошибка", f"Файл метаданных не найден:\n{metadata_file}") | ||||
|                 error_message = "Ошибка: файл метаданных не найден." | ||||
|             else: | ||||
|                 try: | ||||
|                     with open(metadata_file, 'r', encoding='utf-8') as f: | ||||
|                         self.wine_versions_data = json.load(f) | ||||
|                 except (json.JSONDecodeError, IOError) as e: | ||||
|                     QMessageBox.warning(self, "Ошибка", f"Не удалось прочитать или обработать файл метаданных:\n{e}") | ||||
|                     error_message = "Ошибка парсинга JSON." | ||||
|  | ||||
|         if error_message: | ||||
|             self.version_tabs.clear() | ||||
|             error_widget = QWidget() | ||||
|             error_layout = QVBoxLayout(error_widget) | ||||
|             error_label = QLabel(error_message) | ||||
|             error_label.setAlignment(Qt.AlignCenter) | ||||
|             error_layout.addWidget(error_label) | ||||
|             self.version_tabs.addTab(error_widget, "Ошибка") | ||||
|             return | ||||
|  | ||||
|         self.populate_ui() | ||||
|  | ||||
|     def populate_ui(self): | ||||
|         """Заполняет UI отфильтрованными версиями.""" | ||||
|         self.version_tabs.clear() | ||||
|  | ||||
|         is_win64 = self.architecture == "win64" | ||||
|         re_32bit = re.compile(r'i[3-6]86|x86(?!_64)') | ||||
|         re_64bit = re.compile(r'amd64|x86_64|wow64') | ||||
|  | ||||
|         # --- System Tab --- | ||||
|         system_wine_display_name = "system" | ||||
|         if shutil.which('wine'): | ||||
|             try: | ||||
|                 # Пытаемся получить версию системного wine | ||||
|                 result = subprocess.run(['wine', '--version'], capture_output=True, text=True, check=True, encoding='utf-8') | ||||
|                 version_line = result.stdout.strip() | ||||
|                 # Вывод обычно "wine-X.Y.Z" | ||||
|                 system_wine_display_name = f"system ({version_line})" | ||||
|             except (FileNotFoundError, subprocess.CalledProcessError) as e: | ||||
|                 print(f"Не удалось получить версию системного wine: {e}") | ||||
|                 # Если wine возвращает ошибку, просто используем "system" | ||||
|  | ||||
|         self._create_version_tab("Системный", [(system_wine_display_name, "system")]) | ||||
|  | ||||
|         # --- Other versions from JSON --- | ||||
|         group_keys = sorted(self.wine_versions_data.keys()) | ||||
|  | ||||
|         for key in group_keys: | ||||
|             versions = self.wine_versions_data.get(key, []) | ||||
|             all_version_names = {v.get("name", "") for v in versions if v.get("name")} | ||||
|  | ||||
|             filtered_versions = [] | ||||
|             for name in sorted(list(all_version_names), reverse=True): | ||||
|                 if is_win64: | ||||
|                     if re_64bit.search(name) or not re_32bit.search(name): | ||||
|                         filtered_versions.append(name) | ||||
|                 else:  # win32 | ||||
|                     if re_32bit.search(name): | ||||
|                         filtered_versions.append(name) | ||||
|  | ||||
|             if not filtered_versions: | ||||
|                 continue | ||||
|  | ||||
|             pretty_key = key.replace('_', ' ').title() | ||||
|             self._create_version_tab(pretty_key, filtered_versions) | ||||
|  | ||||
|         self.filter_versions() | ||||
|  | ||||
|     def _create_version_tab(self, title, versions_list): | ||||
|         """Создает вкладку с сеткой кнопок для переданного списка версий.""" | ||||
|         tab_page = QWidget() | ||||
|         tab_layout = QVBoxLayout(tab_page) | ||||
|         tab_layout.setContentsMargins(5, 5, 5, 5) | ||||
|  | ||||
|         scroll_area = QScrollArea() | ||||
|         scroll_area.setWidgetResizable(True) | ||||
|         tab_layout.addWidget(scroll_area) | ||||
|  | ||||
|         scroll_content = QWidget() | ||||
|         scroll_area.setWidget(scroll_content) | ||||
|  | ||||
|         grid_layout = QGridLayout(scroll_content) | ||||
|         grid_layout.setAlignment(Qt.AlignTop) | ||||
|  | ||||
|         num_columns = 3 | ||||
|         row, col = 0, 0 | ||||
|         for version_data in versions_list: | ||||
|             if isinstance(version_data, tuple): | ||||
|                 display_name, value_name = version_data | ||||
|             else: | ||||
|                 display_name = value_name = version_data | ||||
|  | ||||
|             btn = QPushButton(display_name) | ||||
|             btn.clicked.connect(partial(self.on_version_selected, value_name)) | ||||
|             grid_layout.addWidget(btn, row, col) | ||||
|             col += 1 | ||||
|             if col >= num_columns: | ||||
|                 col = 0 | ||||
|                 row += 1 | ||||
|  | ||||
|         self.version_tabs.addTab(tab_page, title) | ||||
|  | ||||
|     def filter_versions(self): | ||||
|         """Фильтрует видимость кнопок версий на основе текста поиска.""" | ||||
|         search_text = self.search_edit.text().lower() | ||||
|  | ||||
|         for i in range(self.version_tabs.count()): | ||||
|             tab_widget = self.version_tabs.widget(i) | ||||
|             # The grid layout is inside a scroll area content widget | ||||
|             grid_layout = tab_widget.findChild(QGridLayout) | ||||
|             if not grid_layout: | ||||
|                 continue | ||||
|  | ||||
|             any_visible_in_tab = False | ||||
|             for j in range(grid_layout.count()): | ||||
|                 btn_widget = grid_layout.itemAt(j).widget() | ||||
|                 if isinstance(btn_widget, QPushButton): | ||||
|                     is_match = search_text in btn_widget.text().lower() | ||||
|                     btn_widget.setVisible(is_match) | ||||
|                     if is_match: | ||||
|                         any_visible_in_tab = True | ||||
|  | ||||
|             # Enable/disable tab based on content | ||||
|             self.version_tabs.setTabEnabled(i, any_visible_in_tab) | ||||
|  | ||||
|     def on_version_selected(self, version_name): | ||||
|         """Обрабатывает выбор версии.""" | ||||
|         self.selected_version = version_name | ||||
|         self.accept() | ||||
|  | ||||
| class WineHelperGUI(QMainWindow): | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
| @@ -1063,6 +1262,9 @@ class WineHelperGUI(QMainWindow): | ||||
|         self.icon_animators = {} | ||||
|         self.previous_tab_index = 0 | ||||
|  | ||||
|         # State for command dialog log processing (specifically for prefix creation) | ||||
|         self.command_output_buffer = "" | ||||
|         self.command_last_line_was_progress = False | ||||
|         # Создаем главный виджет и layout | ||||
|         self.main_widget = QWidget() | ||||
|         self.setCentralWidget(self.main_widget) | ||||
| @@ -1085,6 +1287,7 @@ class WineHelperGUI(QMainWindow): | ||||
|         self.create_auto_install_tab() | ||||
|         self.create_manual_install_tab() | ||||
|         self.create_installed_tab() | ||||
|         self.create_prefix_tab() | ||||
|         self.create_help_tab() | ||||
|  | ||||
|         # Инициализируем состояние, которое будет использоваться для логов | ||||
| @@ -1167,13 +1370,13 @@ class WineHelperGUI(QMainWindow): | ||||
|         self.info_panel_layout.setStretch(1, 1) | ||||
|         self.info_panel_layout.setStretch(4, 0) | ||||
|  | ||||
|         if current_tab_text in ["Автоматическая установка", "Ручная установка", "Установленные"]: | ||||
|         if current_tab_text in ["Справка", "Создать префикс"]: | ||||
|             self.info_panel.setVisible(False) | ||||
|         else: | ||||
|             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): | ||||
|         """Создает правую панель с информацией о скрипте""" | ||||
| @@ -1565,6 +1768,81 @@ class WineHelperGUI(QMainWindow): | ||||
|         ) | ||||
|         self.tabs.addTab(installed_tab, "Установленные") | ||||
|  | ||||
|     def open_wine_version_dialog(self): | ||||
|         """Открывает диалог выбора версии Wine.""" | ||||
|         architecture = "win32" if self.arch_win32_radio.isChecked() else "win64" | ||||
|         dialog = WineVersionSelectionDialog(architecture, self.winehelper_path, Var.USER_WORK_PATH, self) | ||||
|         if dialog.exec_() == QDialog.Accepted and dialog.selected_version: | ||||
|             self.wine_version_edit.setText(dialog.selected_version) | ||||
|  | ||||
|     def clear_wine_version_selection(self): | ||||
|         """ | ||||
|         Сбрасывает выбор версии Wine при смене архитектуры, | ||||
|         чтобы заставить пользователя выбрать заново. | ||||
|         """ | ||||
|         self.wine_version_edit.clear() | ||||
|  | ||||
|     def create_prefix_tab(self): | ||||
|         """Создает вкладку для создания нового префикса""" | ||||
|         self.prefix_tab = QWidget() | ||||
|         layout = QVBoxLayout(self.prefix_tab) | ||||
|         layout.setContentsMargins(10, 10, 10, 10) | ||||
|  | ||||
|         form_layout = QFormLayout() | ||||
|         form_layout.setSpacing(10) | ||||
|  | ||||
|         self.prefix_name_edit = QLineEdit() | ||||
|         self.prefix_name_edit.setPlaceholderText("Например: 'm_prefix'") | ||||
|         form_layout.addRow("<b>Имя нового префикса:</b>", self.prefix_name_edit) | ||||
|  | ||||
|         self.arch_groupbox = QGroupBox("Архитектура") | ||||
|         arch_layout = QHBoxLayout() | ||||
|         self.arch_win32_radio = QRadioButton("32-bit") | ||||
|         self.arch_win64_radio = QRadioButton("64-bit") | ||||
|         self.arch_win64_radio.setChecked(True) | ||||
|         arch_layout.addWidget(self.arch_win32_radio) | ||||
|         arch_layout.addWidget(self.arch_win64_radio) | ||||
|         self.arch_groupbox.setLayout(arch_layout) | ||||
|         form_layout.addRow("<b>Разрядность:</b>", self.arch_groupbox) | ||||
|  | ||||
|         self.type_groupbox = QGroupBox("Тип префикса") | ||||
|         type_layout = QHBoxLayout() | ||||
|         self.type_clean_radio = QRadioButton("Чистый") | ||||
|         self.type_recommended_radio = QRadioButton("С рекомендуемыми библиотеками") | ||||
|         self.type_recommended_radio.setChecked(True) | ||||
|         type_layout.addWidget(self.type_clean_radio) | ||||
|         type_layout.addWidget(self.type_recommended_radio) | ||||
|         self.type_groupbox.setLayout(type_layout) | ||||
|         form_layout.addRow("<b>Наполнение:</b>", self.type_groupbox) | ||||
|  | ||||
|         self.wine_version_edit = QLineEdit() | ||||
|         self.wine_version_edit.setReadOnly(True) | ||||
|         self.wine_version_edit.setPlaceholderText("Версия не выбрана") | ||||
|  | ||||
|         select_version_button = QPushButton("Выбрать версию...") | ||||
|         select_version_button.clicked.connect(self.open_wine_version_dialog) | ||||
|  | ||||
|         version_layout = QHBoxLayout() | ||||
|         version_layout.addWidget(self.wine_version_edit) | ||||
|         version_layout.addWidget(select_version_button) | ||||
|         form_layout.addRow("<b>Версия Wine/Proton:</b>", version_layout) | ||||
|  | ||||
|         layout.addLayout(form_layout) | ||||
|         layout.addStretch() | ||||
|  | ||||
|         self.create_prefix_button = QPushButton("Создать префикс") | ||||
|         self.create_prefix_button.setFont(QFont('Arial', 12, QFont.Bold)) | ||||
|         self.create_prefix_button.setStyleSheet("background-color: #0078d7; color: white;") | ||||
|         self.create_prefix_button.setEnabled(False) | ||||
|         self.create_prefix_button.clicked.connect(self.start_prefix_creation) | ||||
|         layout.addWidget(self.create_prefix_button) | ||||
|  | ||||
|         self.tabs.addTab(self.prefix_tab, "Создать префикс") | ||||
|  | ||||
|         self.arch_win32_radio.toggled.connect(self.clear_wine_version_selection) | ||||
|         self.prefix_name_edit.textChanged.connect(self.update_create_prefix_button_state) | ||||
|         self.wine_version_edit.textChanged.connect(self.update_create_prefix_button_state) | ||||
|  | ||||
|     def create_help_tab(self): | ||||
|         """Создает вкладку 'Справка' с подвкладками""" | ||||
|         help_tab = QWidget() | ||||
| @@ -1685,6 +1963,101 @@ class WineHelperGUI(QMainWindow): | ||||
|  | ||||
|         self.tabs.addTab(help_tab, "Справка") | ||||
|  | ||||
|     def update_create_prefix_button_state(self): | ||||
|         """Включает или выключает кнопку 'Создать префикс' в зависимости от заполнения полей.""" | ||||
|         name_ok = bool(self.prefix_name_edit.text().strip()) | ||||
|         version_ok = bool(self.wine_version_edit.text().strip()) | ||||
|         self.create_prefix_button.setEnabled(name_ok and version_ok) | ||||
|  | ||||
|     def start_prefix_creation(self): | ||||
|         """Запускает создание префикса после валидации.""" | ||||
|         if not self._show_license_agreement_dialog(): | ||||
|             return | ||||
|  | ||||
|         prefix_name = self.prefix_name_edit.text().strip() | ||||
|  | ||||
|         if not prefix_name: | ||||
|             QMessageBox.warning(self, "Ошибка", "Имя префикса не может быть пустым.") | ||||
|             return | ||||
|  | ||||
|         if not re.match(r'^[a-zA-Z0-9_.-]+$', prefix_name): | ||||
|             QMessageBox.warning(self, "Ошибка", "Имя префикса может содержать только латинские буквы, цифры, точки, дефисы и подчеркивания.") | ||||
|             return | ||||
|  | ||||
|         prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name) | ||||
|         if os.path.exists(prefix_path): | ||||
|             QMessageBox.warning(self, "Ошибка", f"Префикс с именем '{prefix_name}' уже существует.") | ||||
|             return | ||||
|  | ||||
|         wine_arch = "win32" if self.arch_win32_radio.isChecked() else "win64" | ||||
|         base_pfx = "none" if self.type_clean_radio.isChecked() else "" | ||||
|         wine_use = self.wine_version_edit.text() | ||||
|  | ||||
|         self.command_dialog = QDialog(self) | ||||
|         self.command_dialog.setWindowTitle(f"Создание префикса: {prefix_name}") | ||||
|         self.command_dialog.setMinimumSize(750, 400) | ||||
|         self.command_dialog.setModal(True) | ||||
|         self.command_dialog.setWindowFlags(self.command_dialog.windowFlags() & ~Qt.WindowCloseButtonHint) | ||||
|  | ||||
|         layout = QVBoxLayout() | ||||
|         self.command_log_output = QTextEdit() | ||||
|         self.command_log_output.setReadOnly(True) | ||||
|         self.command_log_output.setFont(QFont('DejaVu Sans Mono', 10)) | ||||
|         layout.addWidget(self.command_log_output) | ||||
|  | ||||
|         self.command_close_button = QPushButton("Закрыть") | ||||
|         self.command_close_button.setEnabled(False) | ||||
|         self.command_close_button.clicked.connect(self.command_dialog.close) | ||||
|         layout.addWidget(self.command_close_button) | ||||
|         self.command_dialog.setLayout(layout) | ||||
|  | ||||
|         # Сбрасываем состояние для обработки лога с прогрессом | ||||
|         self.command_output_buffer = "" | ||||
|         self.command_last_line_was_progress = False | ||||
|  | ||||
|         self.command_process = QProcess(self.command_dialog) | ||||
|         self.command_process.setProcessChannelMode(QProcess.MergedChannels) | ||||
|  | ||||
|         # Для создания префикса используем специальный обработчик вывода с поддержкой прогресс-бара | ||||
|         self.command_process.readyReadStandardOutput.connect(self._handle_prefix_creation_output) | ||||
|         self.command_process.finished.connect(self._handle_prefix_creation_finished) | ||||
|  | ||||
|         env = QProcessEnvironment.systemEnvironment() | ||||
|         env.insert("WINEPREFIX", prefix_path) | ||||
|         env.insert("WINEARCH", wine_arch) | ||||
|         env.insert("WH_WINE_USE", wine_use) | ||||
|         if base_pfx: | ||||
|             env.insert("BASE_PFX", base_pfx) | ||||
|         self.command_process.setProcessEnvironment(env) | ||||
|  | ||||
|         args = ["init-prefix"] | ||||
|         self.command_log_output.append(f"=== Параметры создания префикса ===\nИмя: {prefix_name}\nПуть: {prefix_path}\nАрхитектура: {wine_arch}\nВерсия Wine: {wine_use}\nТип: {'Чистый' if base_pfx else 'С рекомендуемыми библиотеками'}\n\n" + "="*40 + "\n") | ||||
|         self.command_log_output.append(f"Выполнение: {shlex.quote(self.winehelper_path)} {' '.join(shlex.quote(a) for a in args)}") | ||||
|         self.command_process.start(self.winehelper_path, args) | ||||
|         self.command_dialog.exec_() | ||||
|  | ||||
|     def _handle_prefix_creation_finished(self, exit_code, exit_status): | ||||
|         """Обрабатывает завершение создания префикса.""" | ||||
|         # Обрабатываем остаток в буфере, если он есть | ||||
|         if self.command_output_buffer: | ||||
|             self._process_command_log_line(self.command_output_buffer) | ||||
|             self.command_output_buffer = "" | ||||
|  | ||||
|         # Если последней строкой был прогресс, "завершаем" его переносом строки. | ||||
|         if self.command_last_line_was_progress: | ||||
|             cursor = self.command_log_output.textCursor() | ||||
|             cursor.movePosition(QTextCursor.End) | ||||
|             cursor.insertText("\n") | ||||
|             self.command_last_line_was_progress = False | ||||
|  | ||||
|         prefix_name = self.command_process.processEnvironment().value('WINEPREFIX').split('/')[-1] | ||||
|  | ||||
|         self._handle_command_finished(exit_code, exit_status) | ||||
|         if exit_code == 0: | ||||
|             self.prefix_name_edit.clear() | ||||
|             self.wine_version_edit.clear() | ||||
|             QMessageBox.information(self, "Успех", f"Префикс '{prefix_name}' успешно создан.") | ||||
|  | ||||
|     def update_installed_apps(self): | ||||
|         """Обновляет список установленных приложений в виде кнопок""" | ||||
|         # Если активная кнопка находится в списке удаляемых, сбрасываем ее | ||||
| @@ -1891,7 +2264,6 @@ class WineHelperGUI(QMainWindow): | ||||
|         self.command_close_button.clicked.connect(self.command_dialog.close) | ||||
|         layout.addWidget(self.command_close_button) | ||||
|         self.command_dialog.setLayout(layout) | ||||
|  | ||||
|         # Устанавливаем родителя, чтобы избежать утечек памяти | ||||
|         self.command_process = QProcess(self.command_dialog) | ||||
|         self.command_process.setProcessChannelMode(QProcess.MergedChannels) | ||||
| @@ -1935,7 +2307,6 @@ class WineHelperGUI(QMainWindow): | ||||
|         self.command_close_button.clicked.connect(self.command_dialog.close) | ||||
|         layout.addWidget(self.command_close_button) | ||||
|         self.command_dialog.setLayout(layout) | ||||
|  | ||||
|         # Устанавливаем родителя, чтобы избежать утечек памяти | ||||
|         self.command_process = QProcess(self.command_dialog) | ||||
|         self.command_process.setProcessChannelMode(QProcess.MergedChannels) | ||||
| @@ -2332,6 +2703,55 @@ class WineHelperGUI(QMainWindow): | ||||
|         self.installed_global_action_widget.setVisible(False) | ||||
|         self.install_button.setText(f"Установить «{display_name}»") | ||||
|  | ||||
|     def _show_license_agreement_dialog(self): | ||||
|         """Показывает модальный диалог с лицензионным соглашением.""" | ||||
|         dialog = QDialog(self) | ||||
|         dialog.setWindowTitle("Лицензионное соглашение") | ||||
|         dialog.setMinimumSize(750, 400) | ||||
|         dialog.setModal(True) | ||||
|  | ||||
|         layout = QVBoxLayout(dialog) | ||||
|  | ||||
|         license_text = QTextBrowser() | ||||
|         try: | ||||
|             license_file_path = Var.LICENSE_AGREEMENT_FILE | ||||
|             if not license_file_path or not os.path.exists(license_file_path): | ||||
|                 raise FileNotFoundError | ||||
|  | ||||
|             with open(license_file_path, 'r', encoding='utf-8') as f: | ||||
|                 license_content = f.read() | ||||
|  | ||||
|             escaped_license_content = html.escape(license_content) | ||||
|             license_text.setHtml(f""" | ||||
|                 <pre style="font-family: sans-serif; font-size: 10pt; white-space: pre-wrap; word-wrap: break-word;">{escaped_license_content}</pre> | ||||
|             """) | ||||
|         except (FileNotFoundError, TypeError): | ||||
|             license_text.setHtml(f'<h3>Лицензионные соглашения</h3><p>Не удалось загрузить файл лицензионного соглашения по пути:<br>{Var.LICENSE_AGREEMENT_FILE}</p>') | ||||
|         except Exception as e: | ||||
|             license_text.setHtml(f'<h3>Лицензионные соглашения</h3><p>Произошла ошибка при чтении файла лицензии:<br>{str(e)}</p>') | ||||
|  | ||||
|         layout.addWidget(license_text) | ||||
|  | ||||
|         checkbox = QCheckBox("Я принимаю условия лицензионного соглашения") | ||||
|         layout.addWidget(checkbox) | ||||
|  | ||||
|         button_layout = QHBoxLayout() | ||||
|         accept_button = QPushButton("Принять") | ||||
|         accept_button.setEnabled(False) | ||||
|         accept_button.clicked.connect(dialog.accept) | ||||
|  | ||||
|         cancel_button = QPushButton("Отклонить") | ||||
|         cancel_button.clicked.connect(dialog.reject) | ||||
|  | ||||
|         button_layout.addStretch() | ||||
|         button_layout.addWidget(accept_button) | ||||
|         button_layout.addWidget(cancel_button) | ||||
|         layout.addLayout(button_layout) | ||||
|  | ||||
|         checkbox.stateChanged.connect(lambda state: accept_button.setEnabled(state == Qt.Checked)) | ||||
|  | ||||
|         return dialog.exec_() == QDialog.Accepted | ||||
|  | ||||
|     def install_current_script(self): | ||||
|         """Устанавливает текущий выбранный скрипт""" | ||||
|         if not self.current_script: | ||||
| @@ -2499,10 +2919,11 @@ class WineHelperGUI(QMainWindow): | ||||
|             QMessageBox.critical(self.install_dialog, "Ошибка", f"Не удалось запустить установку:\n{str(e)}") | ||||
|             self.cleanup_process() | ||||
|  | ||||
|     def append_log(self, text, is_error=False, add_newline=True): | ||||
|         """Добавляет сообщение в лог""" | ||||
|         if not hasattr(self, 'log_output'): return | ||||
|         cursor = self.log_output.textCursor() | ||||
|     def _append_to_log(self, log_widget, text, is_error=False, add_newline=True): | ||||
|         """Helper to append text to a QTextEdit log widget.""" | ||||
|         if not log_widget: | ||||
|             return | ||||
|         cursor = log_widget.textCursor() | ||||
|         cursor.movePosition(QTextCursor.End) | ||||
|  | ||||
|         if is_error: | ||||
| @@ -2513,9 +2934,13 @@ class WineHelperGUI(QMainWindow): | ||||
|             formatted_text = f"{text}\n" if add_newline else text | ||||
|             cursor.insertText(formatted_text) | ||||
|  | ||||
|         self.log_output.ensureCursorVisible() | ||||
|         log_widget.ensureCursorVisible() | ||||
|         QApplication.processEvents() | ||||
|  | ||||
|     def append_log(self, text, is_error=False, add_newline=True): | ||||
|         """Добавляет сообщение в лог установки.""" | ||||
|         self._append_to_log(self.log_output, text, is_error, add_newline) | ||||
|  | ||||
|     def _process_log_line(self, line_with_delimiter): | ||||
|         """Обрабатывает одну строку лога, управляя заменой строк прогресса.""" | ||||
|         is_progress_line = '\r' in line_with_delimiter | ||||
| @@ -2552,6 +2977,42 @@ class WineHelperGUI(QMainWindow): | ||||
|  | ||||
|         self.last_line_was_progress = is_progress_line | ||||
|  | ||||
|     def _process_command_log_line(self, line_with_delimiter): | ||||
|         """Обрабатывает одну строку лога для диалога создания префикса, управляя заменой строк прогресса.""" | ||||
|         is_progress_line = '\r' in line_with_delimiter | ||||
|  | ||||
|         # Фильтруем "мусорные" строки прогресса (например, '-=O=-' от wget), | ||||
|         # обрабатывая только те, что содержат знак процента. | ||||
|         if is_progress_line: | ||||
|             if not re.search(r'\d\s*%', line_with_delimiter): | ||||
|                 return  # Игнорируем строку прогресса без процентов | ||||
|  | ||||
|         clean_line = line_with_delimiter.replace('\r', '').replace('\n', '').strip() | ||||
|  | ||||
|         if not clean_line: | ||||
|             return | ||||
|  | ||||
|         cursor = self.command_log_output.textCursor() | ||||
|  | ||||
|         # Если новая строка - это прогресс, и предыдущая тоже была прогрессом, | ||||
|         # то мы удаляем старую, чтобы заменить ее новой. | ||||
|         if is_progress_line and self.command_last_line_was_progress: | ||||
|             cursor.movePosition(QTextCursor.End) | ||||
|             cursor.select(QTextCursor.LineUnderCursor) | ||||
|             cursor.removeSelectedText() | ||||
|         elif not is_progress_line and self.command_last_line_was_progress: | ||||
|             # Это переход от строки прогресса к финальной строке. | ||||
|             # Вместо добавления переноса, мы заменяем предыдущую строку новой. | ||||
|             cursor.movePosition(QTextCursor.End) | ||||
|             cursor.select(QTextCursor.LineUnderCursor) | ||||
|             cursor.removeSelectedText() | ||||
|  | ||||
|         # Добавляем новую очищенную строку. | ||||
|         # Для прогресса - без переноса строки, для обычных строк - с переносом. | ||||
|         self._append_to_log(self.command_log_output, clean_line, add_newline=not is_progress_line) | ||||
|  | ||||
|         self.command_last_line_was_progress = is_progress_line | ||||
|  | ||||
|     def handle_process_output(self): | ||||
|         """Обрабатывает вывод процесса, корректно отображая однострочный прогресс.""" | ||||
|         new_data = self.install_process.readAllStandardOutput().data().decode('utf-8', errors='ignore') | ||||
| @@ -2604,6 +3065,27 @@ class WineHelperGUI(QMainWindow): | ||||
|         # Кнопка прервать | ||||
|         self.btn_abort.setEnabled(False) | ||||
|  | ||||
|     def _handle_prefix_creation_output(self): | ||||
|         """Обрабатывает вывод процесса создания префикса, корректно отображая прогресс.""" | ||||
|         if not hasattr(self, 'command_process') or not self.command_process: | ||||
|             return | ||||
|         new_data = self.command_process.readAllStandardOutput().data().decode('utf-8', errors='ignore') | ||||
|         self.command_output_buffer += new_data | ||||
|  | ||||
|         while True: | ||||
|             # Ищем ближайший разделитель (\n или \r) | ||||
|             idx_n = self.command_output_buffer.find('\n') | ||||
|             idx_r = self.command_output_buffer.find('\r') | ||||
|  | ||||
|             if idx_n == -1 and idx_r == -1: | ||||
|                 break  # Нет полных строк для обработки | ||||
|  | ||||
|             split_idx = min(idx for idx in [idx_n, idx_r] if idx != -1) | ||||
|  | ||||
|             line = self.command_output_buffer[:split_idx + 1] | ||||
|             self.command_output_buffer = self.command_output_buffer[split_idx + 1:] | ||||
|             self._process_command_log_line(line) | ||||
|  | ||||
|         # Процесс завершен, можно запланировать его удаление и очистить ссылку, | ||||
|         # чтобы избежать утечек и висячих ссылок. | ||||
|         if self.install_process: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user