Merge branch 'minergenon-devel'

This commit is contained in:
Mikhail Tergoev
2025-10-23 11:11:50 +03:00
3 changed files with 137 additions and 57 deletions

7
GENERAL Normal file
View File

@@ -0,0 +1,7 @@
# Руководство пользователя
Подробное и актуальное руководство по использованию WineHelper смотрите на сайте: https://www.altlinux.org/Winehelper
# Совместимость ПО и сертификаты
С полным списком совместимого ПО и сертификатами можно ознакомиться по следующим ссылкам:
Для 10 платформы: https://www.basealt.ru/fileadmin/user_upload/compatibility/P10-view2.html
Для 11 платформы: https://www.basealt.ru/fileadmin/user_upload/compatibility/P11-view2.html

View File

@@ -7,7 +7,7 @@ if [[ $(id -u) -eq 0 ]] ; then
fi fi
##### DEFAULT PATH ##### ##### DEFAULT PATH #####
export SCRIPT_NAME USER_WORK_PATH RUN_SCRIPT DATA_PATH CHANGELOG_FILE WH_ICON_PATH LICENSE_FILE AGREEMENT THIRD_PARTY_FILE export SCRIPT_NAME USER_WORK_PATH RUN_SCRIPT DATA_PATH CHANGELOG_FILE WH_ICON_PATH LICENSE_FILE AGREEMENT THIRD_PARTY_FILE WH_ICON_TRAY GENERAL
SCRIPT_NAME="$(basename "$0")" SCRIPT_NAME="$(basename "$0")"
if [[ "$(realpath "$0")" == "/usr/bin/$SCRIPT_NAME" ]] ; then if [[ "$(realpath "$0")" == "/usr/bin/$SCRIPT_NAME" ]] ; then
@@ -17,11 +17,12 @@ if [[ "$(realpath "$0")" == "/usr/bin/$SCRIPT_NAME" ]] ; then
RUN_SCRIPT="/usr/bin/$SCRIPT_NAME" RUN_SCRIPT="/usr/bin/$SCRIPT_NAME"
DATA_PATH="/usr/share/$SCRIPT_NAME" DATA_PATH="/usr/share/$SCRIPT_NAME"
WH_ICON_PATH="/usr/share/icons/hicolor/scalable/apps/winehelper.svg" WH_ICON_PATH="/usr/share/icons/hicolor/scalable/apps/winehelper.svg"
WH_ICON_TRAY=" /usr/share/icons/hicolor/symbolic/apps/winehelper-symbolic.svg" WH_ICON_TRAY="/usr/share/icons/hicolor/symbolic/apps/winehelper-symbolic.svg"
CHANGELOG_FILE="/usr/share/doc/winehelper-$WH_VERSION/CHANGELOG" CHANGELOG_FILE="/usr/share/doc/winehelper-$WH_VERSION/CHANGELOG"
LICENSE_FILE="/usr/share/doc/winehelper-$WH_VERSION/LICENSE" LICENSE_FILE="/usr/share/doc/winehelper-$WH_VERSION/LICENSE"
AGREEMENT="/usr/share/doc/winehelper-$WH_VERSION/LICENSE_AGREEMENT" AGREEMENT="/usr/share/doc/winehelper-$WH_VERSION/LICENSE_AGREEMENT"
THIRD_PARTY_FILE="/usr/share/doc/winehelper-$WH_VERSION/THIRD-PARTY" THIRD_PARTY_FILE="/usr/share/doc/winehelper-$WH_VERSION/THIRD-PARTY"
GENERAL="/usr/share/doc/winehelper-$WH_VERSION/GENERAL"
else else
# переменные для тестового запуска WineHelper из репозитория # переменные для тестового запуска WineHelper из репозитория
USER_WORK_PATH="$HOME/test-$SCRIPT_NAME" USER_WORK_PATH="$HOME/test-$SCRIPT_NAME"
@@ -33,6 +34,7 @@ else
LICENSE_FILE="$DATA_PATH/LICENSE" LICENSE_FILE="$DATA_PATH/LICENSE"
AGREEMENT="$DATA_PATH/LICENSE_AGREEMENT" AGREEMENT="$DATA_PATH/LICENSE_AGREEMENT"
THIRD_PARTY_FILE="$DATA_PATH/THIRD-PARTY" THIRD_PARTY_FILE="$DATA_PATH/THIRD-PARTY"
GENERAL="$DATA_PATH/GENERAL"
WH_DEVEL="1" WH_DEVEL="1"
# минимальная проверка синтаксиса скриптов # минимальная проверка синтаксиса скриптов

View File

@@ -13,8 +13,8 @@ from functools import partial
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QTabWidget, QTabBar, from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QTabWidget, QTabBar,
QTextEdit, QFileDialog, QMessageBox, QLineEdit, QCheckBox, QStackedWidget, QScrollArea, QFormLayout, QGroupBox, QRadioButton, QComboBox, QTextEdit, QFileDialog, QMessageBox, QLineEdit, QCheckBox, QStackedWidget, QScrollArea, QFormLayout, QGroupBox, QRadioButton, QComboBox,
QListWidget, QListWidgetItem, QGridLayout, QFrame, QDialog, QTextBrowser, QInputDialog, QDialogButtonBox, QSystemTrayIcon, QMenu) QListWidget, QListWidgetItem, QGridLayout, QFrame, QDialog, QTextBrowser, QInputDialog, QDialogButtonBox, QSystemTrayIcon, QMenu)
from PyQt5.QtCore import Qt, QProcess, QSize, QTimer, QProcessEnvironment, QPropertyAnimation, QEasingCurve, pyqtSignal from PyQt5.QtCore import Qt, QProcess, QSize, QTimer, QProcessEnvironment, QPropertyAnimation, QEasingCurve, pyqtSignal, QRect
from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter, QCursor, QTextCharFormat from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter, QCursor, QTextCharFormat, QColor, QPalette
from PyQt5.QtNetwork import QLocalServer, QLocalSocket from PyQt5.QtNetwork import QLocalServer, QLocalSocket
@@ -26,9 +26,11 @@ class Var:
DATA_PATH = os.environ.get("DATA_PATH") DATA_PATH = os.environ.get("DATA_PATH")
CHANGELOG_FILE = os.environ.get("CHANGELOG_FILE") CHANGELOG_FILE = os.environ.get("CHANGELOG_FILE")
WH_ICON_PATH = os.environ.get("WH_ICON_PATH") WH_ICON_PATH = os.environ.get("WH_ICON_PATH")
WH_ICON_TRAY = os.environ.get("WH_ICON_TRAY")
LICENSE_FILE = os.environ.get("LICENSE_FILE") LICENSE_FILE = os.environ.get("LICENSE_FILE")
LICENSE_AGREEMENT_FILE = os.environ.get("AGREEMENT") LICENSE_AGREEMENT_FILE = os.environ.get("AGREEMENT")
THIRD_PARTY_FILE = os.environ.get("THIRD_PARTY_FILE") THIRD_PARTY_FILE = os.environ.get("THIRD_PARTY_FILE")
GENERAL = os.environ.get("GENERAL")
class DependencyManager: class DependencyManager:
"""Класс для управления проверкой и установкой системных зависимостей.""" """Класс для управления проверкой и установкой системных зависимостей."""
@@ -1737,6 +1739,18 @@ class WineHelperGUI(QMainWindow):
self.raise_() self.raise_()
self.activateWindow() self.activateWindow()
def create_colorized_icon(self, icon_path, color):
"""Загружает иконку (SVG) и применяет к ней указанный цвет."""
target_pixmap = QPixmap(icon_path)
if target_pixmap.isNull():
return QIcon()
painter = QPainter(target_pixmap)
painter.setCompositionMode(QPainter.CompositionMode_SourceIn)
painter.fillRect(target_pixmap.rect(), QColor(color))
painter.end()
return QIcon(target_pixmap)
def create_tray_icon(self): def create_tray_icon(self):
"""Создает и настраивает иконку в системном трее.""" """Создает и настраивает иконку в системном трее."""
if not QSystemTrayIcon.isSystemTrayAvailable(): if not QSystemTrayIcon.isSystemTrayAvailable():
@@ -1745,11 +1759,23 @@ class WineHelperGUI(QMainWindow):
self.tray_icon = QSystemTrayIcon(self) self.tray_icon = QSystemTrayIcon(self)
icon_path = Var.WH_ICON_PATH # --- Определение цвета иконки в зависимости от темы ---
if icon_path and os.path.exists(icon_path): if Var.WH_ICON_TRAY and os.path.exists(Var.WH_ICON_TRAY):
pixmap = QPixmap(icon_path) # Получаем цвет текста окна из палитры приложения.
if not pixmap.isNull(): # Это хороший индикатор для определения контрастного цвета.
self.tray_icon.setIcon(QIcon(pixmap)) window_text_color = self.palette().color(QPalette.WindowText)
# Если цвет текста светлый (высокая яркость), значит, тема темная.
# И наоборот: если цвет текста темный, тема светлая.
# Яркость > 127 обычно указывает на светлый цвет.
is_dark_theme = window_text_color.lightness() > 127
if is_dark_theme:
# Для темных тем используем белую иконку
self.tray_icon.setIcon(self.create_colorized_icon(Var.WH_ICON_TRAY, Qt.white))
else:
# Для светлых тем используем черную иконку
self.tray_icon.setIcon(self.create_colorized_icon(Var.WH_ICON_TRAY, Qt.black))
# Создаем и сохраняем меню как атрибут класса, чтобы оно не удалялось # Создаем и сохраняем меню как атрибут класса, чтобы оно не удалялось
self.tray_menu = QMenu(self) self.tray_menu = QMenu(self)
@@ -1792,10 +1818,7 @@ class WineHelperGUI(QMainWindow):
title = "Автоматическая установка" title = "Автоматическая установка"
html_content = ("<h3>Автоматическая установка</h3>" html_content = ("<h3>Автоматическая установка</h3>"
"<p>Скрипты из этого списка скачают, установят и настроят приложение за вас. Просто выберите программу и нажмите «Установить».</p>" "<p>Скрипты из этого списка скачают, установят и настроят приложение за вас. Просто выберите программу и нажмите «Установить».</p>"
"<p>Для доступа к экспериментальным скриптам установки отметьте опцию <b>«Показать тестовые версии»</b> внизу списка.</p>" "<p>Для доступа к экспериментальным скриптам установки отметьте опцию <b>«Показать тестовые версии»</b> внизу списка.</p>")
"<br><h3>Совместимость с дистрибутивами Альт</h3>"
"<p>С полным списком совместимого ПО и сертификатами (не только для WineHelper) можно ознакомиться по следующим ссылкам:<br>"
"<a href='https://www.basealt.ru/fileadmin/user_upload/compatibility/P10-view2.html'>Для 10 платформы</a> | <a href='https://www.basealt.ru/fileadmin/user_upload/compatibility/P11-view2.html'>Для 11 платформы</a></p>")
show_global = False show_global = False
elif tab_name == "Ручная установка": elif tab_name == "Ручная установка":
title = "Ручная установка" title = "Ручная установка"
@@ -2201,6 +2224,7 @@ class WineHelperGUI(QMainWindow):
self.install_tabs_data['auto'] = { self.install_tabs_data['auto'] = {
'buttons': buttons, 'layout': layout, 'search_edit': search_edit, 'scroll_area': scroll_area 'buttons': buttons, 'layout': layout, 'search_edit': search_edit, 'scroll_area': scroll_area
} }
self.install_tabs_data['auto']['test_buttons'] = []
# Добавляем чекбокс для тестовых версий # Добавляем чекбокс для тестовых версий
test_checkbox = QCheckBox("Показать тестовые версии") test_checkbox = QCheckBox("Показать тестовые версии")
@@ -2236,43 +2260,63 @@ class WineHelperGUI(QMainWindow):
if not data: if not data:
return return
script_folders = ["autoinstall"] is_checked = data['test_checkbox'].isChecked()
if data['test_checkbox'].isChecked(): test_buttons = data.get('test_buttons', [])
script_folders.append("testinstall")
# Перед удалением кнопок останавливаем все связанные с ними таймеры анимации # Если нужно показать тестовые версии и они еще не добавлены
for btn in data['buttons']: if is_checked and not test_buttons:
if btn in self.icon_animators: test_script_folder = "testinstall"
anim_data = self.icon_animators.pop(btn) script_path = os.path.join(Var.DATA_PATH, test_script_folder)
if 'main_timer' in anim_data:
anim_data['main_timer'].stop()
if 'animation' in anim_data and anim_data['animation']:
anim_data['animation'].stop()
# Сбрасываем ссылку на активную кнопку, если она была удалена
if self.current_active_button in data['buttons']:
self.current_active_button = None
# Очищаем старые кнопки и layout
for btn in data['buttons']:
btn.parent().deleteLater()
data['buttons'].clear()
# Заполняем layout новыми кнопками
scripts = []
for folder in script_folders:
script_path = os.path.join(Var.DATA_PATH, folder)
if os.path.isdir(script_path): if os.path.isdir(script_path):
try: try:
folder_scripts = sorted(os.listdir(script_path)) folder_scripts = sorted(os.listdir(script_path))
self._populate_install_grid(data['layout'], folder_scripts, folder, data['buttons']) # Запоминаем, какие кнопки являются тестовыми
scripts.extend(folder_scripts) new_test_buttons = []
except OSError as e: self._populate_install_grid(data['layout'], folder_scripts, test_script_folder, new_test_buttons)
print(f"Не удалось прочитать директорию {script_path}: {e}") data['test_buttons'] = new_test_buttons
data['buttons'].extend(new_test_buttons)
self.autoinstall_scripts.extend(folder_scripts)
self.autoinstall_scripts = scripts # Применяем фильтр и прокручиваем к первому новому элементу
# Применяем текущий фильтр поиска к обновленному списку self.filter_buttons('auto')
self.filter_buttons('auto') if new_test_buttons:
first_new_button = new_test_buttons[0]
frame = first_new_button.parent()
if isinstance(frame, QFrame):
# Даем время на отрисовку перед прокруткой
QTimer.singleShot(100, lambda: data['scroll_area'].ensureWidgetVisible(frame, 50, 50))
except OSError as e:
print(f"Не удалось прочитать директорию {test_script_folder}: {e}")
# Если нужно скрыть тестовые версии и они были добавлены
elif not is_checked and test_buttons:
# Останавливаем анимацию и удаляем виджеты тестовых кнопок
for btn in test_buttons:
if btn in self.icon_animators:
anim_data = self.icon_animators.pop(btn)
if 'main_timer' in anim_data:
anim_data['main_timer'].stop()
if 'animation' in anim_data and anim_data['animation']:
anim_data['animation'].stop()
# Удаляем кнопку из основного списка
if btn in data['buttons']:
data['buttons'].remove(btn)
# Удаляем фрейм кнопки из layout
frame = btn.parent()
if frame:
frame.deleteLater()
# Очищаем список тестовых кнопок
data['test_buttons'].clear()
# Обновляем список скриптов
self.autoinstall_scripts = [s for s in self.autoinstall_scripts if not os.path.exists(os.path.join(Var.DATA_PATH, "testinstall", s))]
# В любом случае применяем фильтр, чтобы скрыть/показать кнопки в соответствии с поиском
if data['test_checkbox'].isChecked():
self.filter_buttons('auto')
def create_installed_tab(self): def create_installed_tab(self):
"""Создает вкладку для отображения установленных программ в виде кнопок""" """Создает вкладку для отображения установленных программ в виде кнопок"""
@@ -3234,17 +3278,44 @@ class WineHelperGUI(QMainWindow):
help_subtabs = QTabWidget() help_subtabs = QTabWidget()
help_layout.addWidget(help_subtabs) help_layout.addWidget(help_subtabs)
# Подвкладка "Руководство" # Подвкладка "Общее"
guide_tab = QWidget() general_tab = QWidget()
guide_layout = QVBoxLayout(guide_tab) general_layout = QVBoxLayout(general_tab)
guide_text = QTextBrowser() general_text = QTextBrowser()
guide_text.setOpenExternalLinks(True) general_text.setOpenExternalLinks(True)
guide_text.setHtml("""
<h2>Руководство пользователя</h2> try:
<p>Подробное и актуальное руководство по использованию WineHelper смотрите на <a href="https://www.altlinux.org/Winehelper">https://www.altlinux.org/Winehelper</a></p> if not Var.GENERAL or not os.path.exists(Var.GENERAL):
""") raise FileNotFoundError
guide_layout.addWidget(guide_text)
help_subtabs.addTab(guide_tab, "Руководство") with open(Var.GENERAL, 'r', encoding='utf-8') as f:
general_content = f.read()
html_content = ""
url_re = re.compile(r'(https?://[^\s]+)')
for line in general_content.splitlines():
line = line.strip()
if not line:
html_content += "<br>"
continue
line = html.escape(line)
line = url_re.sub(r'<a href="\1">\1</a>', line)
if line.startswith('# '):
html_content += f'<h2>{line[2:]}</h2>'
elif line.startswith('Для '):
html_content += f'<p style="margin-left: 10px;">&bull; {line}</p>'
else:
html_content += f'<p>{line}</p>'
general_text.setHtml(html_content)
except (FileNotFoundError, TypeError):
general_text.setHtml(f'<h2>Ошибка</h2><p>Не удалось загрузить файл с общей информацией по пути:<br>{Var.GENERAL}</p>')
general_layout.addWidget(general_text)
help_subtabs.addTab(general_tab, "Общее")
# Подвкладка "Авторы" # Подвкладка "Авторы"
authors_tab = QWidget() authors_tab = QWidget()
@@ -4903,7 +4974,7 @@ def main():
window.server = server window.server = server
window.show() window.show()
# Создаем иконку в системном трее после создания окна # Создаем иконку в системном трее после создания окна
window.create_tray_icon() # window.create_tray_icon() # Временно отключено
return app.exec_() return app.exec_()
return 1 return 1