#!/usr/bin/env python3
import os
import subprocess
import sys
import re
import shlex
import shutil
import html
import time
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,
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.QtNetwork import QLocalServer, QLocalSocket
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")
LICENSE_AGREEMENT_FILE = os.environ.get("AGREEMENT")
class DependencyManager:
"""Класс для управления проверкой и установкой системных зависимостей."""
def __init__(self):
"""Инициализирует менеджер, определяя необходимые пути."""
self.dependencies_script_path = self._get_dependencies_path()
self.config_dir = os.path.join(os.path.expanduser("~"), ".config", "winehelper")
self.hash_flag_file = os.path.join(self.config_dir, "dependencies_hash.txt")
os.makedirs(self.config_dir, exist_ok=True)
self.app_icon = QIcon(Var.WH_ICON_PATH) if Var.WH_ICON_PATH and os.path.exists(Var.WH_ICON_PATH) else QIcon()
def _get_dependencies_path(self):
"""Определяет и возвращает путь к скрипту dependencies.sh."""
winehelper_script_path = os.environ.get("RUN_SCRIPT")
if winehelper_script_path and os.path.exists(winehelper_script_path):
return os.path.join(os.path.dirname(winehelper_script_path), 'dependencies.sh')
return None
def _calculate_file_hash(self):
"""Вычисляет хэш SHA256 файла зависимостей."""
if not self.dependencies_script_path or not os.path.exists(self.dependencies_script_path):
return None
hasher = hashlib.sha256()
try:
with open(self.dependencies_script_path, 'rb') as f:
while chunk := f.read(4096):
hasher.update(chunk)
return hasher.hexdigest()
except IOError:
return None
def _parse_dependencies_from_script(self):
"""
Парсит скрипт dependencies.sh для извлечения списка базовых пакетов.
Возвращает список пакетов или None в случае ошибки.
"""
if not os.path.exists(self.dependencies_script_path):
return None
base_packages = []
try:
with open(self.dependencies_script_path, 'r', encoding='utf-8') as f:
content = f.read()
content = content.replace('\\\n', '')
pattern = r'apt-get install\s+\{i586-,\}(\{.*?\}|[\w.-]+)'
matches = re.findall(pattern, content)
for match in matches:
match = match.strip()
if match.startswith('{') and match.endswith('}'):
group_content = match[1:-1]
packages = [pkg.strip() for pkg in group_content.split(',')]
base_packages.extend(packages)
else:
base_packages.append(match)
return sorted(list(set(pkg for pkg in base_packages if pkg))) or None
except Exception:
return None
def _parse_repo_error_from_script(self):
"""
Парсит скрипт dependencies.sh для извлечения сообщения об ошибке репозитория.
Возвращает сообщение или None в случае ошибки.
"""
if not os.path.exists(self.dependencies_script_path):
return None
try:
with open(self.dependencies_script_path, 'r', encoding='utf-8') as f:
content = f.read()
content = content.replace('\\\n', ' ')
match = re.search(r'apt-repo.*\|\|.*fatal\s+"([^"]+)"', content)
if match:
error_message = match.group(1).strip()
return re.sub(r'\s+', ' ', error_message)
return None
except Exception:
return None
def _show_startup_messages(self):
"""
Проверяет, является ли это первым запуском или обновлением,
и показывает соответствующие сообщения.
"""
current_hash = self._calculate_file_hash()
stored_hash = None
hash_file_exists = os.path.exists(self.hash_flag_file)
if hash_file_exists:
try:
with open(self.hash_flag_file, 'r', encoding='utf-8') as f:
stored_hash = f.read().strip()
except IOError:
pass
if not hash_file_exists:
msg_box = QMessageBox(QMessageBox.Information, "Первый запуск WineHelper",
"Поскольку это первый запуск, программа проверит наличие необходимых системных зависимостей.")
msg_box.setWindowIcon(self.app_icon)
msg_box.exec_()
elif current_hash != stored_hash:
msg_box = QMessageBox(QMessageBox.Information, "Обновление зависимостей",
"Обнаружены изменения в системных требованиях.\n\n"
"Программа выполнит проверку, чтобы убедиться, что все компоненты на месте.")
msg_box.setWindowIcon(self.app_icon)
msg_box.exec_()
def _save_dependency_hash(self):
"""Сохраняет хэш зависимостей в конфигурационный файл."""
current_hash = self._calculate_file_hash()
if not current_hash:
return
try:
with open(self.hash_flag_file, 'w', encoding='utf-8') as f:
f.write(current_hash)
except IOError:
print("Предупреждение: не удалось записать файл с хэшем зависимостей.")
def _perform_check_and_install(self):
"""
Выполняет основную логику проверки и установки зависимостей.
Возвращает True в случае успеха, иначе False.
"""
# Проверка наличия ключевых утилит
if not shutil.which('apt-repo') or not shutil.which('rpm'):
return True
if not self.dependencies_script_path or not os.path.exists(self.dependencies_script_path):
msg_box = QMessageBox(QMessageBox.Critical, "Критическая ошибка",
f"Файл зависимостей не найден по пути:\n'{self.dependencies_script_path}'.\n\n"
"Программа не может продолжить работу."
)
msg_box.setWindowIcon(self.app_icon)
msg_box.exec_()
return False
# 1. Проверка наличия репозитория x86_64-i586
try:
result = subprocess.run(['apt-repo'], capture_output=True, text=True, check=False, encoding='utf-8')
if result.returncode != 0 or 'x86_64-i586' not in result.stdout:
error_message = self._parse_repo_error_from_script()
if not error_message:
msg_box = QMessageBox(QMessageBox.Critical, "Критическая ошибка",
f"Репозиторий x86_64-i586 не подключен, но не удалось извлечь текст ошибки из файла:\n'{self.dependencies_script_path}'.\n\n"
"Проверьте целостность скрипта. Работа программы будет прекращена."
)
msg_box.setWindowIcon(self.app_icon)
msg_box.exec_()
return False
msg_box = QMessageBox(QMessageBox.Critical, "Ошибка репозитория",
f"{error_message}\n\nРабота программы будет прекращена.")
msg_box.setWindowIcon(self.app_icon)
msg_box.exec_()
return False
except FileNotFoundError:
return True
# 2. Определение списка пакетов из dependencies.sh
base_packages = self._parse_dependencies_from_script()
# Если парсинг не удался или скрипт не найден, прерываем работу.
if not base_packages:
msg_box = QMessageBox(QMessageBox.Critical, "Критическая ошибка",
f"Не удалось найти или проанализировать файл зависимостей:\n'{self.dependencies_script_path}'.\n\n"
"Программа не может продолжить работу без списка необходимых пакетов."
)
msg_box.setWindowIcon(self.app_icon)
msg_box.exec_()
return False
required_packages = [f"i586-{pkg}" for pkg in base_packages] + base_packages
# 3. Проверка, какие пакеты отсутствуют
try:
# Запрашиваем список всех установленных пакетов один раз для эффективности
result = subprocess.run(
['rpm', '-qa', '--queryformat', '%{NAME}\n'],
capture_output=True, text=True, check=True, encoding='utf-8'
)
installed_packages_set = set(result.stdout.splitlines())
required_packages_set = set(required_packages)
# Находим разницу между требуемыми и установленными пакетами
missing_packages = sorted(list(required_packages_set - installed_packages_set))
except (subprocess.CalledProcessError, FileNotFoundError) as e:
# В случае ошибки (например, rpm не найден), показываем критическое сообщение
msg_box = QMessageBox(QMessageBox.Critical, "Критическая ошибка",
f"Не удалось получить список установленных пакетов с помощью rpm.\n\nОшибка: {e}\n\n"
"Программа не может проверить зависимости и будет закрыта."
)
msg_box.setWindowIcon(self.app_icon)
msg_box.exec_()
return False
if not missing_packages:
return True
# 4. Запрос у пользователя на установку
msg_box = QMessageBox()
msg_box.setWindowIcon(self.app_icon)
msg_box.setIcon(QMessageBox.Warning)
msg_box.setWindowTitle("Отсутствуют зависимости")
# Устанавливаем формат текста в RichText, чтобы QMessageBox корректно обрабатывал HTML-теги.
msg_box.setTextFormat(Qt.RichText)
# Формируем весь текст как единый HTML-блок для корректного отображения.
full_html_text = (
"Для корректной работы WineHelper требуются дополнительные системные компоненты.
"
"Отсутствуют следующие пакеты:
"
# Ограничиваем высоту блока с пакетами и добавляем прокрутку, если список длинный.
f"""
Установка зависимостей еще не завершена.
" "Вы действительно хотите прервать процесс?
" "Это закроет только окно программы.
"
"Процесс установки зависимостей будет продолжен.
" ) yes_button = msg_box.addButton("Да, прервать", QMessageBox.YesRole) no_button = msg_box.addButton("Нет", QMessageBox.NoRole) msg_box.setDefaultButton(no_button) msg_box.exec_() if msg_box.clickedButton() == yes_button: process.readyRead.disconnect() process.finished.disconnect() process.terminate() event.accept() else: event.ignore() else: event.accept() process.readyRead.connect(handle_output) process.finished.connect(handle_finish) log_output.append(f"Выполнение команды:\npkexec sh -c \"{install_cmd_str}\"\n") log_output.append("Пожалуйста, введите пароль в появившемся окне аутентификации...") log_output.append("-" * 40 + "\n") dialog.closeEvent = dialog_close_handler process.start('pkexec', ['sh', '-c', install_cmd_str]) dialog.exec_() return installation_successful def run(self): """ Основной публичный метод для запуска полной проверки зависимостей. Возвращает True, если все зависимости удовлетворены, иначе False. """ self._show_startup_messages() if self._perform_check_and_install(): self._save_dependency_hash() return True return False class WinetricksManagerDialog(QDialog): """Диалог для управления компонентами Winetricks.""" INFO_TEXT = ( "Компоненты можно только установить либо переустановить.\n" "Удаление компонентов не реализовано в Winetricks.\n" "Для установки нового компонента: Поставьте галочку и нажмите «Применить».\n" "Для переустановки компонента: Выделите его в списке и нажмите кнопку «Переустановить»." ) def __init__(self, prefix_path, winetricks_path, parent=None): super().__init__(parent) self.prefix_path = prefix_path self.winetricks_path = winetricks_path self.initial_states = {} self.apply_process = None self.installation_finished = False self.user_cancelled = False self.processes = {} self.category_statuses = {} self.previous_tab_widget = None self.cache_dir = os.path.join(os.path.expanduser("~"), ".cache", "winehelper", "winetricks") os.makedirs(self.cache_dir, exist_ok=True) self.setWindowTitle(f"Менеджер компонентов для префикса: {os.path.basename(prefix_path)}") self.setMinimumSize(800, 500) # Основной layout main_layout = QVBoxLayout(self) # Табы для категорий self.tabs = QTabWidget() main_layout.addWidget(self.tabs) # Создаем табы self.categories = { "Библиотеки": "dlls", "Шрифты": "fonts", "Настройки": "settings" } self.list_widgets = {} self.search_edits = {} for display_name, internal_name in self.categories.items(): list_widget, search_edit = self._create_category_tab(display_name) self.list_widgets[internal_name] = list_widget self.search_edits[internal_name] = search_edit # Лог для вывода команд self.log_output = QTextEdit() self.log_output.setReadOnly(True) self.log_output.setFont(QFont('DejaVu Sans Mono', 10)) self.log_output.setMaximumHeight(150) self.log_output.setText(self.INFO_TEXT) main_layout.addWidget(self.log_output) # Кнопки управления button_layout = QHBoxLayout() self.status_label = QLabel("Загрузка компонентов...") button_layout.addWidget(self.status_label, 1) self.apply_button = QPushButton("Применить") self.apply_button.setEnabled(False) self.apply_button.clicked.connect(self.apply_changes) button_layout.addWidget(self.apply_button) self.reinstall_button = QPushButton("Переустановить") self.reinstall_button.setEnabled(False) self.reinstall_button.clicked.connect(self.reinstall_selected) button_layout.addWidget(self.reinstall_button) self.close_button = QPushButton("Закрыть") self.close_button.clicked.connect(self.close) button_layout.addWidget(self.close_button) main_layout.addLayout(button_layout) # Подключаем сигнал после создания всех виджетов, чтобы избежать ошибки атрибута self.tabs.currentChanged.connect(self.on_tab_switched) # Загружаем данные self.load_all_categories() # Устанавливаем начальное состояние для отслеживания покинутой вкладки self.previous_tab_widget = self.tabs.currentWidget() def on_tab_switched(self, index): """ Обрабатывает переключение вкладок. Если установка только что завершилась, сбрасывает лог к информационному тексту. """ # Очищаем поле поиска на той вкладке, которую покинули. if self.previous_tab_widget: search_edit = self.previous_tab_widget.findChild(QLineEdit) if search_edit: search_edit.clear() if self.installation_finished: self.log_output.setText(self.INFO_TEXT) self.installation_finished = False self._update_ui_state() # Сохраняем текущую вкладку для следующего переключения self.previous_tab_widget = self.tabs.widget(index) def _create_category_tab(self, title): """Создает вкладку с поиском и списком.""" tab = QWidget() layout = QVBoxLayout(tab) search_edit = QLineEdit() search_edit.setPlaceholderText("Поиск...") layout.addWidget(search_edit) list_widget = QListWidget() list_widget.itemChanged.connect(self._on_item_changed) list_widget.currentItemChanged.connect(self._update_ui_state) layout.addWidget(list_widget) search_edit.textChanged.connect(lambda text, lw=list_widget: self.filter_list(text, lw)) self.tabs.addTab(tab, title) return list_widget, search_edit def filter_list(self, text, list_widget): """Фильтрует элементы в списке.""" for i in range(list_widget.count()): item = list_widget.item(i) item.setHidden(text.lower() not in item.text().lower()) def load_all_categories(self): """Запускает загрузку всех категорий.""" self.loading_count = len(self.categories) self.category_statuses = {name: "загрузка..." for name in self.categories.keys()} for internal_name in self.categories.values(): self._start_load_process(internal_name) def _get_cache_path(self, category): """Возвращает путь к файлу кэша для указанной категории.""" return os.path.join(self.cache_dir, f"{category}.json") def _get_winetricks_hash(self): """Вычисляет хэш файла winetricks для проверки его обновления.""" try: hasher = hashlib.sha256() with open(self.winetricks_path, 'rb') as f: while chunk := f.read(4096): hasher.update(chunk) return hasher.hexdigest() except (IOError, OSError): return None def _start_load_process(self, category): """Запускает QProcess для получения списка компонентов, используя кэш.""" cache_path = self._get_cache_path(category) cache_ttl_seconds = 86400 # 24 часа # Попытка прочитать из кэша if os.path.exists(cache_path): try: with open(cache_path, 'r', encoding='utf-8') as f: cache_data = json.load(f) cache_age = time.time() - cache_data.get("timestamp", 0) winetricks_hash = self._get_winetricks_hash() if cache_age < cache_ttl_seconds and cache_data.get("hash") == winetricks_hash: QTimer.singleShot(0, lambda: self._on_load_finished( category, 0, QProcess.NormalExit, from_cache=cache_data.get("output") )) return except (json.JSONDecodeError, IOError, KeyError): self._log(f"--- Кэш для '{category}' поврежден, будет выполнена перезагрузка. ---") process = QProcess(self) self.processes[category] = process process.setProcessChannelMode(QProcess.MergedChannels) env = QProcessEnvironment.systemEnvironment() env.insert("WINEPREFIX", self.prefix_path) # Отключаем winemenubuilder, чтобы избежать зависаний, связанных с 'wineserver -w'. # Это известная проблема при запуске winetricks из ГУИ. process.setProcessEnvironment(env) # Используем functools.partial для надежной привязки категории к слоту. # Это стандартный и самый надежный способ избежать проблем с замыканием в цикле. process.finished.connect(partial(self._on_load_finished, category)) process.start(self.winetricks_path, [category, "list"]) def _update_status_label(self): """Обновляет текстовую метку состояния загрузки.""" status_parts = [] for name, status in self.category_statuses.items(): status_parts.append(f"{name}: {status}") self.status_label.setText(" | ".join(status_parts)) def _parse_winetricks_log(self): """Читает winetricks.log и возвращает множество установленных компонентов.""" installed_verbs = set() log_path = os.path.join(self.prefix_path, "winetricks.log") if not os.path.exists(log_path): return installed_verbs try: with open(log_path, 'r', encoding='utf-8') as f: for line in f: verb = line.split('#', 1)[0].strip() if verb: installed_verbs.add(verb) except Exception as e: self._log(f"--- Предупреждение: не удалось прочитать {log_path}: {e} ---") return installed_verbs def _parse_winetricks_list_output(self, output, installed_verbs, list_widget): """Парсит вывод 'winetricks list' и заполняет QListWidget.""" # Regex, который обрабатывает строки как с префиксом статуса '[ ]', так и без него. # 1. `(?:\[(.)]\s+)?` - опциональная группа для статуса (напр. '[x]'). # 2. `([^\s]+)` - имя компонента (без пробелов). # 3. `(.*)` - оставшаяся часть строки (описание). line_re = re.compile(r"^\s*(?:\[(.)]\s+)?([^\s]+)\s*(.*)") found_items = False for line in output.splitlines(): match = line_re.match(line) if not match: continue found_items = True _status, name, description = match.groups() # Удаляем из описания информацию о доступности для скачивания, так как она избыточна description = re.sub(r'\[\s*доступно для скачивания[^]]*]', '', description) description = re.sub(r'\[\s*в кэше\s*]', '', description) # Фильтруем служебные строки, которые могут быть ошибочно распознаны. # Имена компонентов winetricks не содержат слэшей и не являются командами. if '/' in name or '\\' in name or name.lower() in ('executing', 'using', 'warning:') or name.endswith(':'): continue is_checked = name in installed_verbs item_text = f"{name.ljust(27)}{description.strip()}" item = QListWidgetItem(item_text) item.setData(Qt.UserRole, name) item.setFont(QFont("DejaVu Sans Mono", 10)) item.setFlags(item.flags() | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Checked if is_checked else Qt.Unchecked) list_widget.addItem(item) self.initial_states[name] = is_checked return found_items def _on_load_finished(self, category, exit_code, exit_status, from_cache=None): """Обрабатывает завершение загрузки списка компонентов.""" if from_cache is not None: output = from_cache process = None else: process = self.processes[category] output = process.readAllStandardOutput().data().decode('utf-8', 'ignore') list_widget = self.list_widgets[category] category_display_name = next(k for k, v in self.categories.items() if v == category) # Очищаем список перед заполнением. list_widget.clear() if exit_code != 0 or exit_status != QProcess.NormalExit: error_string = process.errorString() if process else "N/A" self._log(f"--- Ошибка загрузки категории '{category}' (код: {exit_code}) ---", "red") self.category_statuses[category_display_name] = "ошибка" self._update_status_label() # Показываем ошибку в статусе if exit_status == QProcess.CrashExit: self._log("--- Процесс winetricks завершился аварийно. ---", "red") # По умолчанию используется "Неизвестная ошибка", которая не очень полезна. if error_string != "Неизвестная ошибка": self._log(f"--- Системная ошибка: {error_string} ---", "red") self._log(output if output.strip() else "Winetricks не вернул вывод. Проверьте, что он работает корректно.") self._log("--------------------------------------------------", "red") else: self.category_statuses[category_display_name] = "готово" installed_verbs = self._parse_winetricks_log() # Обновляем статус только если это была сетевая загрузка if from_cache is None: self._update_status_label() found_items = self._parse_winetricks_list_output(output, installed_verbs, list_widget) if from_cache is None: # Только если мы не читали из кэша # Сохраняем успешный результат в кэш cache_path = self._get_cache_path(category) winetricks_hash = self._get_winetricks_hash() if winetricks_hash: cache_data = { "timestamp": time.time(), "hash": winetricks_hash, "output": output } try: with open(cache_path, 'w', encoding='utf-8') as f: json.dump(cache_data, f, ensure_ascii=False, indent=2) except (IOError, OSError) as e: self._log(f"--- Не удалось сохранить кэш для '{category}': {e} ---") if not found_items and output.strip(): self._log(f"--- Не удалось распознать вывод для категории '{category}' ---") self._log(output) self._log("--------------------------------------------------") self.loading_count -= 1 if self.loading_count == 0: self.status_label.setText("Готово.") self._update_ui_state() def _on_item_changed(self, item): """Обрабатывает изменение состояния чекбокса, предотвращая снятие галочки с установленных.""" name = item.data(Qt.UserRole) # Если компонент был изначально установлен и пользователь пытается его снять if name in self.initial_states and self.initial_states.get(name) is True: if item.checkState() == Qt.Unchecked: # Блокируем сигналы, чтобы избежать рекурсии, и возвращаем галочку на место. list_widget = item.listWidget() if list_widget: list_widget.blockSignals(True) item.setCheckState(Qt.Checked) if list_widget: list_widget.blockSignals(False) self._update_ui_state() def _update_ui_state(self, *args): """Централизованно обновляет состояние кнопок 'Применить' и 'Переустановить'.""" # 1. Проверяем, есть ли изменения в чекбоксах (установка новых или снятие галочек с новых) has_changes = False for list_widget in self.list_widgets.values(): for i in range(list_widget.count()): item = list_widget.item(i) name = item.data(Qt.UserRole) if name in self.initial_states: initial_state = self.initial_states[name] current_state = item.checkState() == Qt.Checked if current_state != initial_state: has_changes = True break if has_changes: break self.apply_button.setEnabled(has_changes) # 2. Проверяем, можно ли переустановить выбранный компонент is_reinstallable = False # Переустановка возможна только если нет других изменений if not has_changes: current_list_widget = self.tabs.currentWidget().findChild(QListWidget) if current_list_widget: current_item = current_list_widget.currentItem() if current_item: name = current_item.data(Qt.UserRole) # Компонент можно переустановить, если он был изначально установлен if self.initial_states.get(name, False): is_reinstallable = True self.reinstall_button.setEnabled(is_reinstallable) def reinstall_selected(self): """Переустанавливает выбранный компонент.""" current_list_widget = self.tabs.currentWidget().findChild(QListWidget) if not current_list_widget: return current_item = current_list_widget.currentItem() if not current_item: return name = current_item.data(Qt.UserRole) if not name: return self.log_output.setText(self.INFO_TEXT) self.apply_button.setEnabled(False) self.reinstall_button.setEnabled(False) self.close_button.setEnabled(False) # Установка будет форсированной verbs_to_reinstall = [name] self._start_install_process(verbs_to_reinstall) def apply_changes(self): """Применяет выбранные изменения.""" # Собираем все компоненты, которые были отмечены для установки. verbs_to_install = [] for list_widget in self.list_widgets.values(): for i in range(list_widget.count()): item = list_widget.item(i) name = item.data(Qt.UserRole) if name not in self.initial_states: continue initial_state = self.initial_states[name] current_state = item.checkState() == Qt.Checked if current_state != initial_state: verbs_to_install.append(name) if not verbs_to_install: QMessageBox.information(self, "Нет изменений", "Не выбрано ни одного компонента для установки.") return self.log_output.setText(self.INFO_TEXT) self.apply_button.setEnabled(False) self.reinstall_button.setEnabled(False) self.close_button.setEnabled(False) self._start_install_process(verbs_to_install) def _start_install_process(self, verbs_to_install): """Запускает процесс установки/переустановки winetricks.""" # Добавляем флаг --force, чтобы разрешить переустановку self._log(f"Выполнение установки: winetricks --unattended --force {' '.join(verbs_to_install)}") self.apply_process = QProcess(self) self.apply_process.setProcessChannelMode(QProcess.MergedChannels) env = QProcessEnvironment.systemEnvironment() env.insert("WINEPREFIX", self.prefix_path) self.apply_process.setProcessEnvironment(env) self.apply_process.readyReadStandardOutput.connect(lambda: self.log_output.append(self.apply_process.readAllStandardOutput().data().decode('utf-8', 'ignore').strip())) self.apply_process.finished.connect(self.on_apply_finished) self.apply_process.start(self.winetricks_path, ["--unattended", "--force"] + verbs_to_install) def on_apply_finished(self, exit_code, exit_status): """Обрабатывает завершение применения изменений.""" # 1. Проверяем, была ли отмена пользователем if self.user_cancelled: self._log("\n=== Установка прервана пользователем. ===") self._show_message_box("Отмена", "Установки компонентов прервана пользователем.", QMessageBox.Warning, {"buttons": {"Да": QMessageBox.AcceptRole}}) # Сбрасываем флаг и восстанавливаем UI self.user_cancelled = False self.apply_button.setEnabled(True) self.close_button.setEnabled(True) return # 2. Обрабатываем реальную ошибку if exit_code != 0 or exit_status != QProcess.NormalExit: self._log(f"\n=== Ошибка во время выполнения операций (код: {exit_code}). ===", "red") self._show_message_box("Ошибка", "Произошла ошибка во время выполнения операций.\n" "Подробности смотрите в логе.", QMessageBox.Warning, {"buttons": {"OK": QMessageBox.AcceptRole}}) self.apply_button.setEnabled(True) self.close_button.setEnabled(True) return # 3. Обрабатываем успех self._log("\n=== Все операции успешно завершены ===") self._show_message_box("Успех", "Операции с компонентами были успешно выполнены.", QMessageBox.Information, {"buttons": {"Да": QMessageBox.AcceptRole}}) self.apply_button.setEnabled(True) self.reinstall_button.setEnabled(False) # Сбрасываем в неактивное состояние self.close_button.setEnabled(True) # Очищаем все поля поиска. for search_edit in self.search_edits.values(): search_edit.clear() # Перезагружаем данные, чтобы обновить состояние self.status_label.setText("Обновление данных...") self.initial_states.clear() self.load_all_categories() self.installation_finished = True def closeEvent(self, event): """Обрабатывает закрытие окна, чтобы предотвратить выход во время установки.""" # Проверяем, запущен ли процесс установки/переустановки if self.apply_process and self.apply_process.state() == QProcess.Running: reply = self._show_message_box('Подтверждение', "Процесс установки еще не завершен. Вы уверены, что хотите прервать его?", QMessageBox.Question, {"buttons": {"Да": QMessageBox.YesRole, "Нет": QMessageBox.NoRole}, "default": "Нет"}) if reply == "Да": self.user_cancelled = True self.apply_process.terminate() # Попытка мягкого завершения event.accept() # Разрешаем закрытие else: event.ignore() # Запрещаем закрытие else: event.accept() # Процесс не запущен, можно закрывать def _show_message_box(self, title, text, icon, config): """Централизованный метод для создания и показа QMessageBox.""" msg_box = QMessageBox(self) msg_box.setWindowTitle(title) msg_box.setText(text) msg_box.setIcon(icon) buttons = {} for btn_text, role in config.get("buttons", {}).items(): buttons[btn_text] = msg_box.addButton(btn_text, role) default_btn_text = config.get("default") if default_btn_text and default_btn_text in buttons: msg_box.setDefaultButton(buttons[default_btn_text]) msg_box.exec_() clicked_button = msg_box.clickedButton() return clicked_button.text() if clicked_button else None def _log(self, message, color=None): """Добавляет сообщение в лог с возможностью указания цвета.""" if color: self.log_output.append(f'{message}') else: self.log_output.append(message) self.log_output.moveCursor(QTextCursor.End) class WineHelperGUI(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("WineHelper") self.setMinimumSize(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: 8px; padding: 0px; }" self.FRAME_STYLE_SELECTED = "QFrame { border: 2px solid #0078d7; border-radius: 8px; padding: 0px; }" # Основные переменные self.winehelper_path = Var.RUN_SCRIPT self.process = None self.current_script = None self.install_process = None self.current_display_name = None self.install_dialog = None self.current_active_button = None self.installed_buttons = [] self.install_tabs_data = {} self.current_selected_app = None self.icon_animators = {} self.previous_tab_index = 0 # Создаем главный виджет и 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=1) # Создаем панель информации о скрипте self.create_info_panel() self.main_layout.addWidget(self.info_panel, stretch=1) # Фиксируем минимальные размеры self.tabs.setMinimumWidth(520) self.info_panel.setMinimumWidth(415) # Вкладки self.create_auto_install_tab() self.create_manual_install_tab() self.create_installed_tab() self.create_help_tab() # Инициализируем состояние, которое будет использоваться для логов self._reset_log_state() # Обновляем список установленных приложений self.update_installed_apps() # Соединяем сигнал смены вкладок с функцией self.tabs.currentChanged.connect(self.on_tab_changed) # Устанавливаем начальное состояние видимости панели self.on_tab_changed(self.tabs.currentIndex()) def activate(self): """ Активирует и показывает окно приложения, поднимая его из свернутого состояния и перемещая на передний план. """ # Убеждаемся, что окно не свернуто self.setWindowState(self.windowState() & ~Qt.WindowMinimized | Qt.WindowActive) self.show() self.raise_() self.activateWindow() 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.uninstall_button.setVisible(False) self.current_selected_app = None def on_tab_changed(self, index): """Скрывает или показывает панель информации в зависимости от активной вкладки.""" # Очищаем поле поиска на вкладке, которую покинули previous_widget = self.tabs.widget(self.previous_tab_index) if previous_widget: # Ищем QLineEdit в дочерних элементах search_edit = previous_widget.findChild(QLineEdit) if search_edit: search_edit.clear() # Обновляем индекс предыдущей вкладки для следующего переключения self.previous_tab_index = 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', 12, 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', 12, 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) top_buttons_layout.addWidget(self.run_button) installed_action_layout.addLayout(top_buttons_layout) # --- Сетка с утилитами --- utils_grid_layout = QGridLayout() utils_grid_layout.setSpacing(5) # Ряд 0 self.winetricks_button = QPushButton("Менеджер компонентов") self.winetricks_button.clicked.connect(self.open_winetricks_manager) self.winetricks_button.setToolTip("Установка компонентов, библиотек и шрифтов в префикс с помощью Winetricks.") utils_grid_layout.addWidget(self.winetricks_button, 0, 0) self.winecfg_button = QPushButton("Редактор настроек") self.winecfg_button.clicked.connect(lambda: self._run_wine_util('winecfg')) self.winecfg_button.setToolTip("Запуск утилиты winecfg для настройки параметров Wine (версия Windows, диски, аудио и т.д.).") utils_grid_layout.addWidget(self.winecfg_button, 0, 1) # Ряд 1 self.regedit_button = QPushButton("Редактор реестра") self.regedit_button.clicked.connect(lambda: self._run_wine_util('regedit')) self.regedit_button.setToolTip("Запуск редактора реестра Wine (regedit) для просмотра и изменения ключей реестра в префиксе.") utils_grid_layout.addWidget(self.regedit_button, 1, 0) self.uninstaller_button = QPushButton("Удаление программ") self.uninstaller_button.clicked.connect(lambda: self._run_wine_util('uninstaller')) self.uninstaller_button.setToolTip("Запуск стандартного деинсталлятора Wine для удаления установленных в префикс Windows-программ.") utils_grid_layout.addWidget(self.uninstaller_button, 1, 1) # Ряд 2 self.cmd_button = QPushButton("Командная строка") self.cmd_button.clicked.connect(lambda: self._run_wine_util('cmd')) self.cmd_button.setToolTip("Запуск командной строки (cmd) в окружении выбранного префикса.") utils_grid_layout.addWidget(self.cmd_button, 2, 0) self.winefile_button = QPushButton("Файловый менеджер") self.winefile_button.clicked.connect(lambda: self._run_wine_util('winefile')) self.winefile_button.setToolTip("Запуск файлового менеджера Wine (winefile) для просмотра файлов внутри префикса.") utils_grid_layout.addWidget(self.winefile_button, 2, 1) installed_action_layout.addLayout(utils_grid_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.uninstall_button = QPushButton("Удалить префикс") self.uninstall_button.setIcon(QIcon.fromTheme("user-trash")) self.uninstall_button.clicked.connect(self.uninstall_app) installed_global_layout.addWidget(self.uninstall_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) 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 _start_icon_fade_animation(self, button): """Запускает анимацию плавного перехода для иконки на кнопке с помощью QPropertyAnimation.""" if button not in self.icon_animators: return anim_data = self.icon_animators[button] # Получаем или создаем объект анимации один раз animation = anim_data.get('animation') if not animation: # Устанавливаем динамическое свойство, чтобы избежать предупреждений button.setProperty("fadeProgress", 0.0) animation = QPropertyAnimation(button, b"fadeProgress", self) animation.setDuration(700) animation.setEasingCurve(QEasingCurve.InOutQuad) # Сигналы подключаются только один раз при создании animation.valueChanged.connect( lambda value, b=button: self._update_icon_frame(b, value) ) animation.finished.connect( lambda b=button: self._on_fade_animation_finished(b) ) anim_data['animation'] = animation # Останавливаем предыдущую анимацию, если она еще идет if animation.state() == QPropertyAnimation.Running: animation.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] # Сохраняем QPixmap для использования в функции обновления кадра anim_data['pixmaps'] = (QPixmap(current_icon_path), QPixmap(next_icon_path)) # Устанавливаем начальное и конечное значения и запускаем animation.setStartValue(0.0) animation.setEndValue(1.0) animation.start() # Без DeleteWhenStopped def _on_fade_animation_finished(self, button): """Вызывается по завершении анимации для обновления индекса иконки.""" if button in self.icon_animators: anim_data = self.icon_animators[button] anim_data['current_index'] = (anim_data['current_index'] + 1) % len(anim_data['icons']) def _update_icon_frame(self, button, progress): """Обновляет кадр анимации, смешивая две иконки в зависимости от прогресса.""" anim_data = self.icon_animators.get(button) if not anim_data or 'pixmaps' not in anim_data: return old_pixmap, new_pixmap = anim_data['pixmaps'] # На последнем кадре просто устанавливаем новую иконку if progress >= 1.0: 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): """Создает вкладку для автоматической установки программ""" ( scripts, buttons, layout, search_edit, scroll_area ) = self._create_and_populate_install_tab( "Автоматическая установка", "autoinstall", "Поиск скрипта автоматической установки...", partial(self.filter_buttons, 'auto') ) self.autoinstall_scripts = scripts self.install_tabs_data['auto'] = { 'buttons': buttons, 'layout': layout, 'search_edit': search_edit, 'scroll_area': scroll_area } def create_manual_install_tab(self): """Создает вкладку для ручной установки программ""" ( scripts, buttons, layout, search_edit, scroll_area ) = self._create_and_populate_install_tab( "Ручная установка", "manualinstall", "Поиск скрипта ручной установки...", partial(self.filter_buttons, 'manual') ) self.manualinstall_scripts = scripts self.install_tabs_data['manual'] = { 'buttons': buttons, 'layout': layout, 'search_edit': search_edit, 'scroll_area': scroll_area } def create_installed_tab(self): """Создает вкладку для отображения установленных программ в виде кнопок""" 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) title_name = self._get_current_app_title() self.install_dialog.setWindowTitle(f"Установка «{title_name}»") 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_found = False 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"""{escaped_license_content}""") license_found = True except (FileNotFoundError, TypeError): license_text.setHtml(f'
Не удалось загрузить файл лицензионного соглашения по пути:
{Var.LICENSE_AGREEMENT_FILE}
Произошла ошибка при чтении файла лицензии:
{str(e)}