forked from CastroFidel/winehelper
1531 lines
77 KiB
Python
1531 lines
77 KiB
Python
#!/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 = ("<h3>Автоматическая установка</h3>"
|
||
"<p>Скрипты из этого списка скачают, установят и настроят приложение за вас.</p>"
|
||
"<p>Просто выберите программу и нажмите «Установить».</p>")
|
||
show_global = False
|
||
elif tab_name == "Ручная установка":
|
||
title = "Ручная установка"
|
||
html_content = ("<h3>Ручная установка</h3>"
|
||
"<p>Эти скрипты подготовят окружение для установки.</p>"
|
||
"<p>Вам нужно будет указать только путь к установочному файлу (<code>.exe</code> или <code>.msi</code>), который вы скачали самостоятельно и нажать «Установить».</p>")
|
||
show_global = False
|
||
elif tab_name == "Установленные":
|
||
title = "Установленные программы"
|
||
html_content = ("<h3>Установленные программы</h3>"
|
||
"<p>Здесь отображаются все приложения, установленные с помощью WineHelper.</p>"
|
||
"<p>Выберите программу, чтобы увидеть доступные действия.</p>"
|
||
"<p>Также на этой вкладке можно восстановить префикс из резервной копии с помощью соответствующей кнопки.</p>")
|
||
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("""
|
||
<h2>Руководство пользователя</h2>
|
||
<p>Подробное и актуальное руководство по использованию WineHelper смотрите на <a href="https://www.altlinux.org/Winehelper">https://www.altlinux.org/Winehelper</a></p>
|
||
""")
|
||
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("""
|
||
<div style="text-align: center;">
|
||
<h2>Разработчики</h2>
|
||
Михаил Тергоев (fidel)<br>
|
||
Сергей Пальчех (minergenon)</p>
|
||
<p><b>Помощники</b><br>
|
||
Иван Мажукин (vanomj)</p>
|
||
<p><b>Идея и поддержка:</b><br>
|
||
сообщество ALT Linux</p>
|
||
<br>
|
||
<p>Отдельная благодарность всем, кто вносит свой вклад в развитие проекта,<br>
|
||
тестирует и сообщает об ошибках!</p>
|
||
</div>
|
||
""")
|
||
authors_layout.addWidget(authors_text)
|
||
help_subtabs.addTab(authors_tab, "Авторы")
|
||
|
||
# Подвкладка "Лицензия"
|
||
license_tab = QWidget()
|
||
license_layout = QVBoxLayout(license_tab)
|
||
import html
|
||
license_text = QTextBrowser()
|
||
license_text.setOpenExternalLinks(True)
|
||
|
||
try:
|
||
if not Var.LICENSE_FILE or not os.path.exists(Var.LICENSE_FILE):
|
||
raise FileNotFoundError
|
||
|
||
with open(Var.LICENSE_FILE, 'r', encoding='utf-8') as f:
|
||
license_content = f.read()
|
||
|
||
escaped_license_content = html.escape(license_content)
|
||
|
||
license_html = f"""
|
||
<h2>Лицензия</h2>
|
||
<pre style="font-family: 'DejaVu Sans Mono', monospace; font-size: 9pt; padding: 10px; border: 1px solid #a0a0a0; border-radius: 5px;">{escaped_license_content}</pre>
|
||
<hr>
|
||
<h2>Сторонние компоненты</h2>
|
||
<p>Некоторые компоненты, используемые или устанавливаемые данным ПО, могут иметь собственные лицензии. Пользователь несет полную ответственность за соблюдение этих лицензионных соглашений.</p>
|
||
<p>Ниже приведен список основных сторонних компонентов и ссылки на их исходный код:</p>
|
||
"""
|
||
|
||
# Читаем и парсим файл 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 += '<blockquote>'
|
||
for line in third_party_content.splitlines():
|
||
line = line.strip()
|
||
if not line:
|
||
third_party_html += '<br>'
|
||
continue
|
||
escaped_line = html.escape(line)
|
||
if line.startswith('http'):
|
||
third_party_html += f' <a href="{escaped_line}" style="font-size: 10pt;">{escaped_line}</a><br>'
|
||
else:
|
||
third_party_html += f'<b>{escaped_line}</b><br>'
|
||
third_party_html += '</blockquote>'
|
||
|
||
license_text.setHtml(license_html + third_party_html)
|
||
except (FileNotFoundError, TypeError):
|
||
license_text.setHtml(f'<h2>Лицензия</h2><p>Не удалось загрузить файл лицензии по пути:<br>{Var.LICENSE_FILE}</p>')
|
||
except Exception as e:
|
||
license_text.setHtml(f'<h2>Лицензия</h2><p>Произошла ошибка при чтении файла лицензии:<br>{str(e)}</p>')
|
||
|
||
license_layout.addWidget(license_text)
|
||
help_subtabs.addTab(license_tab, "Лицензия")
|
||
|
||
# Подвкладка "История изменений"
|
||
changelog_tab = QWidget()
|
||
changelog_layout = QVBoxLayout(changelog_tab)
|
||
changelog_text = QTextEdit()
|
||
changelog_text.setReadOnly(True)
|
||
changelog_text.setFont(QFont('DejaVu Sans Mono', 9))
|
||
|
||
try:
|
||
if not Var.CHANGELOG_FILE or not os.path.exists(Var.CHANGELOG_FILE):
|
||
raise FileNotFoundError
|
||
with open(Var.CHANGELOG_FILE, 'r', encoding='utf-8') as f:
|
||
changelog_content = f.read()
|
||
changelog_text.setText(changelog_content)
|
||
except (FileNotFoundError, TypeError):
|
||
changelog_text.setText(f"Файл истории изменений не найден по пути:\n{Var.CHANGELOG_FILE}")
|
||
except Exception as e:
|
||
changelog_text.setText(f"Не удалось прочитать файл истории изменений:\n{str(e)}")
|
||
|
||
changelog_layout.addWidget(changelog_text)
|
||
help_subtabs.addTab(changelog_tab, "История изменений")
|
||
|
||
self.tabs.addTab(help_tab, "Справка")
|
||
|
||
def update_installed_apps(self):
|
||
"""Обновляет список установленных приложений в виде кнопок"""
|
||
# Если активная кнопка находится в списке удаляемых, сбрасываем ее
|
||
if self.current_active_button in self.installed_buttons:
|
||
self.current_active_button = None
|
||
|
||
# Очистить существующие кнопки
|
||
for btn in self.installed_buttons:
|
||
btn.deleteLater()
|
||
self.installed_buttons.clear()
|
||
|
||
if not os.path.exists(Var.USER_WORK_PATH):
|
||
os.makedirs(Var.USER_WORK_PATH, exist_ok=True)
|
||
return
|
||
|
||
desktop_files = []
|
||
for entry in os.scandir(Var.USER_WORK_PATH):
|
||
if entry.is_file() and entry.name.endswith('.desktop'):
|
||
desktop_files.append(entry.path)
|
||
|
||
desktop_files.sort()
|
||
|
||
for i, desktop_path in enumerate(desktop_files):
|
||
display_name = os.path.splitext(os.path.basename(desktop_path))[0]
|
||
icon_path = None
|
||
try:
|
||
with open(desktop_path, 'r', encoding='utf-8') as f:
|
||
for line in f:
|
||
if line.startswith('Name='):
|
||
display_name = line.split('=', 1)[1].strip()
|
||
elif line.startswith('Icon='):
|
||
icon_file = line.split('=', 1)[1].strip()
|
||
if os.path.exists(icon_file):
|
||
icon_path = icon_file
|
||
except Exception as e:
|
||
print(f"Error reading {desktop_path}: {str(e)}")
|
||
|
||
btn = self._create_app_button(display_name, [icon_path], self.INSTALLED_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 _, p=desktop_path, b=btn: self.show_installed_app_info(p, b))
|
||
|
||
row = i // 2
|
||
column = i % 2
|
||
self.installed_scroll_layout.addWidget(frame, row, column)
|
||
self.installed_buttons.append(btn)
|
||
|
||
def _set_active_button(self, button_widget):
|
||
"""Устанавливает активную кнопку и обновляет стили ее обертки (QFrame)."""
|
||
# Сброс стиля предыдущей активной кнопки
|
||
if self.current_active_button and self.current_active_button != button_widget:
|
||
parent_frame = self.current_active_button.parent()
|
||
if isinstance(parent_frame, QFrame):
|
||
parent_frame.setStyleSheet(self.FRAME_STYLE_DEFAULT)
|
||
|
||
# Применение стиля к новой кнопке
|
||
parent_frame = button_widget.parent()
|
||
if isinstance(parent_frame, QFrame):
|
||
parent_frame.setStyleSheet(self.FRAME_STYLE_SELECTED)
|
||
|
||
self.current_active_button = button_widget
|
||
|
||
def show_installed_app_info(self, desktop_path, button_widget):
|
||
"""Показывает информацию об установленном приложении в правой панели."""
|
||
self._set_active_button(button_widget)
|
||
# Очищаем поле поиска и принудительно обновляем список, чтобы показать все приложения
|
||
self.installed_search_edit.blockSignals(True)
|
||
self.installed_search_edit.clear()
|
||
self.installed_search_edit.blockSignals(False)
|
||
self.filter_installed_buttons()
|
||
|
||
# Прокручиваем к выбранному элементу
|
||
frame = button_widget.parent()
|
||
if isinstance(frame, QFrame):
|
||
# Даем циклу событий обработать перерисовку перед прокруткой
|
||
QTimer.singleShot(0, lambda: self.installed_scroll_area.ensureWidgetVisible(frame))
|
||
|
||
self.current_selected_app = {'desktop_path': desktop_path}
|
||
|
||
# Меняем растяжение: убираем у описания (индекс 1) и добавляем
|
||
# виджету с действиями для приложения (индекс 4), чтобы он оттолкнул кнопку "Восстановить" вниз.
|
||
self.info_panel_layout.setStretch(1, 0)
|
||
self.info_panel_layout.setStretch(4, 1)
|
||
|
||
try:
|
||
with open(desktop_path, 'r', encoding='utf-8') as f:
|
||
content = f.read()
|
||
# Парсим .desktop файл для получения информации
|
||
name = ""
|
||
comment = ""
|
||
exec_cmd = ""
|
||
icon = ""
|
||
|
||
for line in content.split('\n'):
|
||
if line.startswith('Name='):
|
||
name = line.split('=', 1)[1].strip()
|
||
elif line.startswith('Comment='):
|
||
comment = line.split('=', 1)[1].strip()
|
||
elif line.startswith('Exec='):
|
||
exec_cmd = line.split('=', 1)[1].strip()
|
||
elif line.startswith('Icon='):
|
||
icon = line.split('=', 1)[1].strip()
|
||
|
||
self.current_selected_app['name'] = name
|
||
self.current_selected_app['exec'] = exec_cmd
|
||
|
||
# Показываем панель информации
|
||
self.info_panel.setVisible(True)
|
||
|
||
# Показываем информацию в правой панели
|
||
self.script_title.setPixmap(QPixmap()) # Сначала очищаем иконку, чтобы отобразился текст
|
||
self.script_title.setText(name)
|
||
self.script_description.setVisible(False)
|
||
|
||
# Управляем видимостью кнопок
|
||
self.install_action_widget.setVisible(False)
|
||
self.installed_action_widget.setVisible(True)
|
||
self.installed_global_action_widget.setVisible(True)
|
||
self.backup_button.setVisible(True)
|
||
self.create_log_button.setVisible(True)
|
||
self.manual_install_path_widget.setVisible(False)
|
||
|
||
except Exception as e:
|
||
QMessageBox.warning(self, "Ошибка", f"Не удалось прочитать информацию о приложении: {str(e)}")
|
||
self.current_selected_app = None
|
||
self.info_panel.setVisible(False)
|
||
|
||
def _get_prefix_name_for_selected_app(self):
|
||
"""Извлекает имя префикса для выбранного приложения."""
|
||
if not self.current_selected_app or 'desktop_path' not in self.current_selected_app:
|
||
return None
|
||
|
||
desktop_file = self.current_selected_app['desktop_path']
|
||
if os.path.exists(desktop_file):
|
||
try:
|
||
with open(desktop_file, 'r', encoding='utf-8') as f:
|
||
for line in f:
|
||
if line.startswith('Exec='):
|
||
exec_line = line.strip()
|
||
# Ищем часть пути, например: .../prefixes/some_prefix_name/...
|
||
if "prefixes/" in exec_line:
|
||
prefix_part = exec_line.split("prefixes/")[1].split("/")[0]
|
||
if prefix_part:
|
||
return prefix_part
|
||
except Exception as e:
|
||
print(f"Error getting prefix name from {desktop_file}: {e}")
|
||
return None
|
||
|
||
def backup_prefix_for_app(self):
|
||
"""Создает резервную копию префикса для выбранного приложения."""
|
||
prefix_name = self._get_prefix_name_for_selected_app()
|
||
if not prefix_name:
|
||
QMessageBox.warning(self, "Ошибка", "Не удалось определить префикс для выбранного приложения.")
|
||
return
|
||
|
||
# Создаем кастомные кнопки
|
||
yes_button = QPushButton("Да")
|
||
no_button = QPushButton("Нет")
|
||
|
||
msg_box = QMessageBox()
|
||
msg_box.setWindowTitle("Создание резервной копии")
|
||
msg_box.setText(
|
||
f"Будет создана резервная копия префикса '{prefix_name}'.\n"
|
||
f"Файл будет сохранен на вашем Рабочем столе в формате .whpack.\n\nПродолжить?"
|
||
)
|
||
msg_box.addButton(yes_button, QMessageBox.YesRole)
|
||
msg_box.addButton(no_button, QMessageBox.NoRole)
|
||
msg_box.setDefaultButton(no_button)
|
||
|
||
msg_box.exec_()
|
||
|
||
if msg_box.clickedButton() != yes_button:
|
||
return # Отмена, если нажато "Нет" или крестик
|
||
# Используем модальный диалог для отображения процесса резервного копирования (бэкап)
|
||
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_process = QProcess()
|
||
self.command_process.setProcessChannelMode(QProcess.MergedChannels)
|
||
self.command_process.readyReadStandardOutput.connect(self._handle_command_output)
|
||
self.command_process.finished.connect(self._handle_command_finished)
|
||
|
||
winehelper_path = self.winehelper_path
|
||
args = ["backup-prefix", prefix_name]
|
||
|
||
self.command_log_output.append(f"Выполнение: {shlex.quote(winehelper_path)} {' '.join(shlex.quote(a) for a in args)}")
|
||
self.command_process.start(winehelper_path, args)
|
||
self.command_dialog.exec_()
|
||
|
||
def restore_prefix(self):
|
||
"""Восстанавливает префикс из резервной копии."""
|
||
backup_path, _ = QFileDialog.getOpenFileName(
|
||
self,
|
||
"Выберите файл резервной копии для восстановления",
|
||
os.path.expanduser("~"),
|
||
"WineHelper Backups (*.whpack);;Все файлы (*)"
|
||
)
|
||
|
||
if not backup_path:
|
||
return
|
||
|
||
# Используем модальный диалог для отображения процесса восстановления из бэкапа
|
||
self.command_dialog = QDialog(self)
|
||
self.command_dialog.setWindowTitle(f"Восстановление из: {os.path.basename(backup_path)}")
|
||
self.command_dialog.setMinimumSize(600, 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_process = QProcess()
|
||
self.command_process.setProcessChannelMode(QProcess.MergedChannels)
|
||
self.command_process.readyReadStandardOutput.connect(self._handle_command_output)
|
||
self.command_process.finished.connect(self._handle_restore_finished)
|
||
|
||
winehelper_path = self.winehelper_path
|
||
args = ["restore-prefix", backup_path]
|
||
|
||
self.command_log_output.append(f"Выполнение: {shlex.quote(winehelper_path)} {' '.join(shlex.quote(a) for a in args)}")
|
||
self.command_process.start(winehelper_path, args)
|
||
self.command_dialog.exec_()
|
||
|
||
def run_installed_app_with_debug(self):
|
||
"""Запускает выбранное установленное приложение с созданием лога"""
|
||
# Создаем кастомные кнопки
|
||
yes_button = QPushButton("Да")
|
||
no_button = QPushButton("Нет")
|
||
|
||
msg_box = QMessageBox()
|
||
msg_box.setWindowTitle("Создание лога")
|
||
msg_box.setText(
|
||
"Приложение будет запущено в режиме отладки.\n"
|
||
"После закрытия приложения лог будет сохранен в вашем домашнем каталоге "
|
||
"под именем 'winehelper.log'."
|
||
)
|
||
msg_box.addButton(yes_button, QMessageBox.YesRole)
|
||
msg_box.addButton(no_button, QMessageBox.NoRole)
|
||
msg_box.exec_()
|
||
|
||
if msg_box.clickedButton() == yes_button:
|
||
self._run_app_launcher(debug=True)
|
||
|
||
def run_installed_app(self):
|
||
"""Запускает выбранное установленное приложение"""
|
||
self._run_app_launcher(debug=False)
|
||
|
||
def _run_app_launcher(self, debug=False):
|
||
"""Внутренний метод для запуска приложения (с отладкой или без)"""
|
||
if not self.current_selected_app or 'exec' not in self.current_selected_app:
|
||
QMessageBox.warning(self, "Ошибка", "Сначала выберите приложение.")
|
||
return
|
||
|
||
command_str = self.current_selected_app['exec']
|
||
|
||
try:
|
||
# Используем shlex для безопасного разбора командной строки
|
||
command_parts = shlex.split(command_str)
|
||
|
||
# Удаляем параметры (%F и подобные)
|
||
clean_command = [part for part in command_parts if not part.startswith('%')]
|
||
|
||
if debug:
|
||
# Команда имеет вид: ['env', '/path/to/winehelper', '/path/to/app.exe']
|
||
# Нужно вставить '--debug' после скрипта winehelper
|
||
try:
|
||
script_index = -1
|
||
for i, part in enumerate(clean_command):
|
||
if 'winehelper' in os.path.basename(part):
|
||
script_index = i
|
||
break
|
||
|
||
if script_index != -1:
|
||
clean_command.insert(script_index + 1, '--debug')
|
||
else:
|
||
QMessageBox.critical(self, "Ошибка", "Не удалось найти скрипт winehelper в команде запуска.")
|
||
return
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "Ошибка", f"Не удалось модифицировать команду для отладки: {e}")
|
||
return
|
||
|
||
# Удаление префикса
|
||
try:
|
||
subprocess.Popen(clean_command)
|
||
print(f"Запущено: {' '.join(clean_command)}")
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "Ошибка запуска",
|
||
f"Не удалось запустить команду:\n{' '.join(clean_command)}\n\nОшибка: {str(e)}")
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "Ошибка",
|
||
f"Не удалось обработать команду запуска:\n{command_str}\n\nОшибка: {str(e)}")
|
||
|
||
def uninstall_app(self):
|
||
"""Удаляет выбранное установленное приложение и его префикс"""
|
||
if not self.current_selected_app or 'desktop_path' not in self.current_selected_app:
|
||
QMessageBox.warning(self, "Ошибка", "Сначала выберите приложение.")
|
||
return
|
||
|
||
app_name = self.current_selected_app.get('name', 'это приложение')
|
||
prefix_name = self._get_prefix_name_for_selected_app()
|
||
|
||
if not prefix_name:
|
||
QMessageBox.warning(self, "Ошибка", "Не удалось определить префикс для выбранного приложения.")
|
||
return
|
||
|
||
# Создаем кастомные кнопки
|
||
yes_button = QPushButton("Да")
|
||
no_button = QPushButton("Нет")
|
||
|
||
msg_box = QMessageBox()
|
||
msg_box.setWindowTitle('Подтверждение')
|
||
msg_box.setText(
|
||
f'Вы действительно хотите удалить "{app_name}" и все связанные файлы (включая префикс "{prefix_name}")?'
|
||
)
|
||
msg_box.addButton(yes_button, QMessageBox.YesRole)
|
||
msg_box.addButton(no_button, QMessageBox.NoRole)
|
||
msg_box.setDefaultButton(no_button)
|
||
|
||
msg_box.exec_()
|
||
|
||
if msg_box.clickedButton() == yes_button:
|
||
try:
|
||
# Полный путь к префиксу
|
||
prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
|
||
|
||
# 1. Сначала собираем ВСЕ .desktop файлы, связанные с этим префиксом
|
||
all_desktop_files = set()
|
||
# Добавляем основной .desktop файл
|
||
if 'desktop_path' in self.current_selected_app:
|
||
all_desktop_files.add(self.current_selected_app['desktop_path'])
|
||
|
||
desktop_locations = [
|
||
Var.USER_WORK_PATH,
|
||
os.path.join(os.path.expanduser("~"), ".local/share/applications/WineHelper"),
|
||
os.path.join(os.path.expanduser("~"), "Desktop"),
|
||
os.path.join(os.path.expanduser("~"), "Рабочий стол"),
|
||
]
|
||
|
||
# Проверяем все .desktop файлы в стандартных местах
|
||
for location in desktop_locations:
|
||
if not os.path.exists(location):
|
||
continue
|
||
|
||
for file in os.listdir(location):
|
||
if file.endswith('.desktop'):
|
||
file_path = os.path.join(location, file)
|
||
try:
|
||
with open(file_path, 'r', encoding='utf-8') as f:
|
||
content = f.read()
|
||
if f"prefixes/{prefix_name}/" in content:
|
||
all_desktop_files.add(file_path)
|
||
except:
|
||
continue
|
||
|
||
# 2. Удаляем сам префикс
|
||
try:
|
||
if os.path.exists(prefix_path):
|
||
shutil.rmtree(prefix_path)
|
||
print(f"Удален префикс: {prefix_path}")
|
||
else:
|
||
print(f"Префикс не найден: {prefix_path}")
|
||
except Exception as e:
|
||
raise RuntimeError(f"Ошибка удаления префикса: {str(e)}")
|
||
|
||
# 3. Удаляем ВСЕ найденные .desktop файлы, связанные с этим префиксом
|
||
removed_files = []
|
||
for file_path in all_desktop_files:
|
||
try:
|
||
os.remove(file_path)
|
||
removed_files.append(file_path)
|
||
except Exception as e:
|
||
print(f"Ошибка удаления {file_path}: {str(e)}")
|
||
|
||
# 4. Удаляем категорию меню, если она пуста
|
||
menu_dir = os.path.join(os.path.expanduser("~"), ".local/share/applications/WineHelper")
|
||
if os.path.exists(menu_dir) and not os.listdir(menu_dir):
|
||
try:
|
||
os.rmdir(menu_dir)
|
||
# Удаляем связанные файлы меню
|
||
menu_files = [
|
||
os.path.join(os.path.expanduser("~"),
|
||
".local/share/desktop-directories/WineHelper.directory"),
|
||
os.path.join(os.path.expanduser("~"), ".config/menus/applications-merged/WineHelper.menu")
|
||
]
|
||
for f in menu_files:
|
||
if os.path.exists(f):
|
||
os.remove(f)
|
||
except Exception as e:
|
||
print(f"Ошибка удаления пустой категории меню: {str(e)}")
|
||
|
||
# Обновляем кэш desktop файлов
|
||
try:
|
||
subprocess.run(
|
||
["update-desktop-database", os.path.join(os.path.expanduser("~"), ".local/share/applications")])
|
||
except Exception as e:
|
||
print(f"Ошибка обновления кэша desktop файлов: {str(e)}")
|
||
|
||
# Обновляем список установленных приложений
|
||
self.update_installed_apps()
|
||
|
||
# Формируем отчет об удалении
|
||
report = f"Удаление завершено:\n"
|
||
report += f"- Префикс: {prefix_path}\n"
|
||
report += f"- Удаленные .desktop файлы ({len(removed_files)}):\n"
|
||
report += "\n".join(f" - {f}" for f in removed_files) if removed_files else " (не найдены)"
|
||
|
||
# Создаем кастомный диалог, чтобы кнопка была на русском
|
||
success_box = QMessageBox(self)
|
||
success_box.setWindowTitle("Успех")
|
||
success_box.setText(report)
|
||
success_box.setIcon(QMessageBox.Information)
|
||
success_box.addButton("Готово", QMessageBox.AcceptRole)
|
||
success_box.exec_()
|
||
|
||
self._reset_info_panel_to_default("Установленные")
|
||
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "Ошибка",
|
||
f"Не удалось удалить приложение: {str(e)}\n\n"
|
||
f"Desktop файл: {self.current_selected_app.get('desktop_path', 'не определен')}\n"
|
||
f"Префикс: {prefix_name}\n"
|
||
f"Путь к префиксу: {prefix_path if 'prefix_path' in locals() else 'не определен'}")
|
||
def _filter_buttons_in_grid(self, search_text, button_list, grid_layout):
|
||
"""Общий метод для фильтрации кнопок и перестроения сетки."""
|
||
search_text_lower = search_text.lower()
|
||
visible_frames = []
|
||
for btn in button_list:
|
||
frame = btn.parent()
|
||
if isinstance(frame, QFrame):
|
||
# Сначала скрываем все, чтобы правильно перестроить сетку
|
||
frame.setVisible(False)
|
||
if search_text_lower in btn.text().lower():
|
||
visible_frames.append(frame)
|
||
|
||
# Перестраиваем сетку только с видимыми элементами
|
||
for i, frame in enumerate(visible_frames):
|
||
row, column = divmod(i, 2)
|
||
grid_layout.addWidget(frame, row, column)
|
||
frame.setVisible(True)
|
||
|
||
def filter_installed_buttons(self):
|
||
"""Фильтрует кнопки установленных программ."""
|
||
self._filter_buttons_in_grid(
|
||
self.installed_search_edit.text(), self.installed_buttons, self.installed_scroll_layout
|
||
)
|
||
|
||
def filter_manual_buttons(self):
|
||
"""Фильтрует кнопки ручной установки."""
|
||
self._filter_buttons_in_grid(
|
||
self.manual_search_edit.text(), self.manualinstall_buttons, self.manual_scroll_layout
|
||
)
|
||
|
||
def extract_info_ru(self, script_path):
|
||
"""Извлекает информацию из строки # info_ru: в скрипте"""
|
||
try:
|
||
with open(script_path, 'r', encoding='utf-8') as f:
|
||
for line in f:
|
||
if line.startswith('# info_ru:'):
|
||
return line.replace('# info_ru:', '').strip()
|
||
return "Описание отсутствует"
|
||
except Exception as e:
|
||
return f"Ошибка чтения файла: {str(e)}"
|
||
|
||
def show_script_info(self, script_name, button_widget):
|
||
"""Показывает информацию о выбранном скрипте"""
|
||
self._set_active_button(button_widget)
|
||
self.current_script = script_name
|
||
|
||
# Определяем виджеты и действия в зависимости от типа скрипта
|
||
if script_name in self.autoinstall_scripts:
|
||
script_path = os.path.join(Var.DATA_PATH, "autoinstall", script_name)
|
||
search_edit = self.search_edit
|
||
filter_func = self.filter_autoinstall_buttons
|
||
scroll_area = self.auto_scroll_area
|
||
self.manual_install_path_widget.setVisible(False)
|
||
else:
|
||
script_path = os.path.join(Var.DATA_PATH, "manualinstall", script_name)
|
||
search_edit = self.manual_search_edit
|
||
filter_func = self.filter_manual_buttons
|
||
scroll_area = self.manual_scroll_area
|
||
self.manual_install_path_widget.setVisible(True)
|
||
|
||
# Общая логика: очищаем поиск, обновляем список и прокручиваем к элементу
|
||
search_edit.blockSignals(True)
|
||
search_edit.clear()
|
||
search_edit.blockSignals(False)
|
||
filter_func()
|
||
frame = button_widget.parent()
|
||
if isinstance(frame, QFrame):
|
||
QTimer.singleShot(0, lambda: scroll_area.ensureWidgetVisible(frame))
|
||
|
||
# Обновляем информацию в правой панели
|
||
description = self.extract_info_ru(script_path)
|
||
icon_names = self.extract_icons_from_script(script_path)
|
||
prog_name = self.extract_prog_name_from_script(script_path)
|
||
prog_url = self.extract_prog_url_from_script(script_path)
|
||
display_name = prog_name if prog_name else script_name
|
||
|
||
if icon_names:
|
||
# Для заголовка используем первую иконку из списка
|
||
icon_path = os.path.join(Var.DATA_PATH, "image", f"{icon_names[0]}.png")
|
||
if os.path.exists(icon_path):
|
||
self.script_title.setPixmap(QPixmap(icon_path).scaled(64, 64, Qt.KeepAspectRatio))
|
||
else:
|
||
self.script_title.setPixmap(QPixmap())
|
||
else:
|
||
self.script_title.setPixmap(QPixmap())
|
||
|
||
self.script_title.setText(display_name)
|
||
|
||
html_description = f"<p>{description}</p>"
|
||
if prog_url:
|
||
html_description += f'<p><b>Официальный сайт:</b> <a href="{prog_url}">{prog_url}</a></p>'
|
||
|
||
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"""
|
||
<h3>Лицензионные соглашения использования сторонних компонентов:</h3>
|
||
<p>{license_content}</p>
|
||
""")
|
||
except Exception as e:
|
||
print(f"Ошибка чтения файла для извлечения лицензии: {str(e)}")
|
||
license_text.setHtml("""
|
||
<h3>Лицензионные соглашения</h3>
|
||
<p>Не удалось загрузить текст лицензионного соглашения.</p>
|
||
""")
|
||
|
||
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'<font color="red">{text}</font><br>')
|
||
else:
|
||
cursor.insertText(f"{text}\n")
|
||
|
||
self.log_output.ensureCursorVisible()
|
||
QApplication.processEvents()
|
||
|
||
def handle_process_output(self):
|
||
"""Обрабатывает вывод процесса"""
|
||
output = self.install_process.readAllStandardOutput().data().decode('utf-8', errors='ignore').strip()
|
||
if output:
|
||
self.append_log(output)
|
||
|
||
def handle_process_finished(self, exit_code, exit_status):
|
||
"""Обрабатывает завершение процесса"""
|
||
if exit_code == 0 and exit_status == QProcess.NormalExit:
|
||
self.append_log("\n=== Установка успешно завершена ===")
|
||
# Создаем кастомный диалог, чтобы кнопка была на русском
|
||
success_box = QMessageBox(self.install_dialog)
|
||
success_box.setWindowTitle("Успех")
|
||
success_box.setText(f"Программа {self.current_script} установлена успешно!")
|
||
success_box.setIcon(QMessageBox.Information)
|
||
success_box.addButton("Готово", QMessageBox.AcceptRole)
|
||
success_box.exec_()
|
||
|
||
self.update_installed_apps()
|
||
|
||
# Кнопка закрыть
|
||
self.btn_close.setEnabled(True)
|
||
# Кнопка прервать
|
||
self.btn_abort.setEnabled(False)
|
||
|
||
def handle_install_dialog_close(self, event):
|
||
"""Обрабатывает событие закрытия диалога установки."""
|
||
# Проверяем, запущен ли еще процесс установки
|
||
if hasattr(self, 'install_process') and self.install_process and self.install_process.state() == QProcess.Running:
|
||
yes_button = QPushButton("Да, прервать")
|
||
no_button = QPushButton("Нет")
|
||
|
||
msg_box = QMessageBox(self.install_dialog)
|
||
msg_box.setWindowTitle("Прервать установку?")
|
||
msg_box.setText("Установка еще не завершена.\nВы действительно хотите прервать процесс?")
|
||
msg_box.setIcon(QMessageBox.Question)
|
||
msg_box.addButton(yes_button, QMessageBox.YesRole)
|
||
msg_box.addButton(no_button, QMessageBox.NoRole)
|
||
msg_box.setDefaultButton(no_button)
|
||
|
||
msg_box.exec_()
|
||
|
||
if msg_box.clickedButton() == yes_button:
|
||
self.append_log("\n=== Пользователь прервал установку через закрытие окна ===", is_error=True)
|
||
# Завершаем процесс. Сигнал finished вызовет handle_process_finished,
|
||
# который обновит состояние кнопок.
|
||
self.install_process.terminate()
|
||
event.accept() # Разрешаем закрытие окна
|
||
else:
|
||
# Пользователь нажал "Нет", поэтому игнорируем событие закрытия
|
||
event.ignore()
|
||
else:
|
||
# Процесс не запущен (установка завершена или еще не началась),
|
||
# поэтому просто закрываем окно
|
||
event.accept()
|
||
|
||
def abort_installation(self):
|
||
"""Прерывает текущую установку"""
|
||
if hasattr(self, 'install_process') and self.install_process and self.install_process.state() == QProcess.Running:
|
||
yes_button = QPushButton("Да")
|
||
no_button = QPushButton("Нет")
|
||
msg_box = QMessageBox()
|
||
msg_box.setWindowTitle("Подтверждение")
|
||
msg_box.setText("Вы действительно хотите прервать установку?")
|
||
msg_box.addButton(yes_button, QMessageBox.YesRole)
|
||
msg_box.addButton(no_button, QMessageBox.NoRole)
|
||
msg_box.setDefaultButton(no_button)
|
||
msg_box.exec_()
|
||
if msg_box.clickedButton() == yes_button:
|
||
self.append_log("\n=== Пользователь прервал установку ===", is_error=True)
|
||
self.cleanup_process()
|
||
|
||
def _handle_command_output(self):
|
||
"""Обрабатывает вывод для диалога команды"""
|
||
if hasattr(self, 'command_process') and self.command_process:
|
||
output = self.command_process.readAllStandardOutput().data().decode('utf-8', errors='ignore').strip()
|
||
if output and hasattr(self, 'command_log_output'):
|
||
self.command_log_output.append(output)
|
||
QApplication.processEvents()
|
||
|
||
def _handle_command_finished(self, exit_code, exit_status):
|
||
"""Обрабатывает завершение для диалога команды"""
|
||
if exit_code == 0:
|
||
self.command_log_output.append(f"\n=== Команда успешно завершена ===")
|
||
else:
|
||
self.command_log_output.append(f"\n=== Ошибка выполнения (код: {exit_code}) ===")
|
||
self.command_close_button.setEnabled(True)
|
||
|
||
def _handle_restore_finished(self, exit_code, exit_status):
|
||
"""Обрабатывает завершение для диалога команды восстановления."""
|
||
if exit_code == 0:
|
||
self.command_log_output.append(f"\n=== Восстановление успешно завершено ===")
|
||
self.update_installed_apps()
|
||
self.filter_installed_buttons()
|
||
else:
|
||
self.command_log_output.append(f"\n=== Ошибка выполнения (код: {exit_code}) ===")
|
||
self.command_close_button.setEnabled(True)
|
||
|
||
def cleanup_process(self):
|
||
"""Очищает ресурсы процесса, принудительно завершая его, если он активен."""
|
||
if hasattr(self, 'install_process') and self.install_process:
|
||
if self.install_process.state() == QProcess.Running:
|
||
self.install_process.terminate()
|
||
# Даем процессу 3 секунды на завершение
|
||
if not self.install_process.waitForFinished(3000):
|
||
self.append_log("Процесс не ответил на terminate, отправляется kill...", is_error=True)
|
||
self.install_process.kill()
|
||
self.install_process.waitForFinished() # Ждем завершения после kill
|
||
self.install_process.deleteLater()
|
||
self.install_process = None
|
||
|
||
def filter_autoinstall_buttons(self):
|
||
"""Фильтрует кнопки автоматической установки."""
|
||
self._filter_buttons_in_grid(
|
||
self.search_edit.text(), self.autoinstall_buttons, self.scroll_layout
|
||
)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
app = QApplication(sys.argv)
|
||
window = WineHelperGUI()
|
||
window.show()
|
||
sys.exit(app.exec_())
|