Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3dbfea89e3 | ||
|
|
daa13f2e4c | ||
|
|
2aca05928a | ||
|
|
8b2af7015b | ||
|
|
4315cd0e3f | ||
|
|
fa10a3cf0e | ||
|
|
9f932c4403 | ||
|
|
b725be78d1 | ||
|
|
7cbd3cffc7 | ||
|
|
190698a1b4 | ||
|
|
c96033311c | ||
|
|
0a225d4c3d | ||
|
|
18b92b66b1 | ||
|
|
230315bc57 | ||
|
|
3d981158b0 | ||
|
|
4ad1e38141 | ||
|
|
ae24b2386e | ||
|
|
8b5b199ff1 | ||
|
|
af291e9c7a | ||
|
|
c8cb60e42c | ||
|
|
4d508afb44 | ||
|
|
f36869bea5 | ||
|
|
0e311b5d07 | ||
|
|
cfbb6030b5 | ||
|
|
8661d0258d | ||
|
|
9bb7dc7db0 | ||
|
|
1f08c75cb2 | ||
|
|
17802d2bc5 | ||
|
|
86694b3721 | ||
|
|
ad37601b23 | ||
|
|
a6d894ce99 | ||
|
|
8877012ec4 | ||
|
|
5774e033d4 | ||
|
|
de8294b34d | ||
|
|
273e86958d | ||
|
|
d0f3d0f8d6 | ||
|
|
fc01de74b2 | ||
|
|
eb5d6bb613 | ||
|
|
e65cd2386c | ||
|
|
6b094f4905 | ||
|
|
7f44432388 | ||
|
|
fbfccd0417 | ||
|
|
1942d49f4d | ||
|
|
ab2933f8b0 | ||
|
|
4b985417c4 | ||
|
|
d52bec4798 | ||
|
|
f2c7ecf926 | ||
|
|
a929411daf | ||
|
|
8114b3cb54 | ||
|
|
04e974a0b7 | ||
|
|
efe8ef7267 | ||
|
|
a4c6ef89bf | ||
|
|
2bfb574ba1 |
No files matched your search
@@ -0,0 +1,48 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
.venv/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.project
|
||||
.pydevproject
|
||||
.settings/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.bak
|
||||
@@ -0,0 +1,232 @@
|
||||
# WineHelper - Руководство для AI агентов
|
||||
|
||||
## Обзор проекта
|
||||
|
||||
WineHelper — инструмент для упрощения установки Windows-приложений на Linux. Использует подготовленные скрипты, портативные версии Wine и изолированные префиксы, обеспечивая максимальную идентичность на различных операционных системах, основанных на ALT Linux (начиная с релиза p10 и выше).
|
||||
|
||||
**Лицензия:** GNU Lesser General Public License v2.1 (LGPL-2.1+)
|
||||
|
||||
**Автор:** Mikhail Tergoev (2025)
|
||||
|
||||
**Язык документации и комментариев:** Русский
|
||||
|
||||
**Статус проекта:** WIP (work in progress)
|
||||
|
||||
## Технологический стек
|
||||
|
||||
### Основные технологии
|
||||
- **Bash** — основной язык для CLI-интерфейса (~2692 строк в главном скрипте)
|
||||
- **Python 3** — графический интерфейс с использованием PyQt5
|
||||
- **Wine** — запуск Windows-приложений (поддерживаются различные версии: vanilla, staging, Proton, tkg, crossover)
|
||||
- **DXVK/VKD3D** — трансляция DirectX → Vulkan
|
||||
|
||||
### Зависимости Python
|
||||
- PyQt5 (основной GUI фреймворк)
|
||||
- Стандартная библиотека Python (os, subprocess, sys, re, shlex, shutil, html)
|
||||
|
||||
### Системные зависимости
|
||||
- i586-версии библиотек (для 32-битных приложений, если не используется wow64)
|
||||
- wget/curl для загрузки
|
||||
- 7z, tar для распаковки
|
||||
- wrestool, icotool для извлечения иконок из .exe
|
||||
- winetricks для управления компонентами Wine
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
winehelper/
|
||||
├── winehelper # Главный bash-скрипт (CLI)
|
||||
├── winehelper_gui.py # GUI на PyQt5 (точка входа)
|
||||
├── winehelperqt/ # Python-модуль GUI
|
||||
│ ├── __init__.py # Экспорты основных классов
|
||||
│ ├── dialogs/ # Диалоговые окна
|
||||
│ │ ├── base_version_dialog.py
|
||||
│ │ ├── component_version.py
|
||||
│ │ ├── create_prefix.py # Создание префикса
|
||||
│ │ ├── file_associations.py
|
||||
│ │ ├── settings.py # Настройки
|
||||
│ │ ├── wine_version.py # Выбор версии Wine
|
||||
│ │ └── winetricks_manager.py
|
||||
│ ├── parsers/ # Парсеры
|
||||
│ │ ├── script_parser.py # Парсинг скриптов установки
|
||||
│ │ ├── sha256_parser.py # Парсинг sha256sum.list
|
||||
│ │ └── winetricks_parser.py
|
||||
│ ├── services/ # Бизнес-логика
|
||||
│ │ ├── application_installer.py
|
||||
│ │ ├── desktop_file_manager.py
|
||||
│ │ ├── prefix_manager.py # Управление префиксами
|
||||
│ │ ├── process_handler.py
|
||||
│ │ └── winetricks_service.py
|
||||
│ ├── utils/ # Утилиты
|
||||
│ │ └── log_handler.py # Обработка логов
|
||||
│ └── widgets/ # Кастомные виджеты
|
||||
├── autoinstall/ # Скрипты автоматической установки (43 приложения)
|
||||
├── manualinstall/ # Скрипты ручной установки (9 приложений)
|
||||
├── testinstall/ # Тестовые скрипты установки
|
||||
├── database/ # Базы данных префиксов (.whdb файлы)
|
||||
├── image/ # Иконки приложений и GUI ресурсы
|
||||
│ ├── gui/ # Иконки приложения (SVG)
|
||||
│ └── handbook/ # Скриншоты для документации
|
||||
├── hiden_scripts/ # Скрытые/вспомогательные скрипты
|
||||
├── auto_completion/ # Автодополнение для bash
|
||||
├── dependencies.sh # Установка системных зависимостей
|
||||
├── sha256sum.list # SHA256 хеши загружаемых файлов
|
||||
└── winehelper.desktop # Desktop-файл для запуска GUI
|
||||
```
|
||||
|
||||
## Архитектура кода
|
||||
|
||||
### CLI (winehelper)
|
||||
|
||||
Главный скрипт `winehelper` — монолитный bash-скрипт с функциональными блоками:
|
||||
|
||||
1. **Переменные и окружение** — определение путей, версий, флагов
|
||||
2. **Функции сообщений** — цветной вывод в терминал
|
||||
3. **Утилиты** — работа с файлами, архивами, скачивание
|
||||
4. **Управление префиксами** — создание, удаление, бэкап/восстановление
|
||||
5. **Wine management** — установка версий Wine, DXVK, VKD3D
|
||||
6. **Desktop integration** — создание .desktop файлов, меню
|
||||
7. **Основной цикл** — обработка аргументов командной строки
|
||||
|
||||
### GUI (winehelperqt)
|
||||
|
||||
Модульная архитектура PyQt5:
|
||||
|
||||
- **WineHelperGUI** — главное окно, управление вкладками
|
||||
- **PrefixManager** — CRUD операции с префиксами
|
||||
- **ApplicationInstaller** — установка приложений
|
||||
- **ScriptParser** — парсинг информации из скриптов установки
|
||||
- **Диалоги** — модальные окна для настроек и выбора
|
||||
|
||||
### Скрипты установки
|
||||
|
||||
Каждый скрипт установки — это bash-скрипт с переменными:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# info_ru: Описание программы
|
||||
export PROG_URL="https://example.com"
|
||||
export WH_WINE_USE="wine-10.18.1-alt1-wow64" # Версия Wine
|
||||
export WINEPREFIX="prefix_name" # Имя префикса
|
||||
export PROG_NAME="Название программы"
|
||||
export BASE_PFX="base_prefix_v01" # Базовый шаблон (опционально)
|
||||
export WINEARCH="win64" # Архитектура (win32/win64)
|
||||
export INSTALL_DLL="corefonts msxml3..." # DLL через winetricks
|
||||
|
||||
# Логика установки
|
||||
prepair_wine
|
||||
try_download "URL" "${FILE_PATH}"
|
||||
unpack "${FILE_PATH}" "${DEST_DIR}"
|
||||
wine_run_install "${SETUP_EXE}" /S
|
||||
create_desktop "Имя ярлыка" "${EXE_PATH}" "icon_name"
|
||||
```
|
||||
|
||||
## Режимы работы
|
||||
|
||||
### Системный режим
|
||||
Когда скрипт установлен в `/usr/bin/winehelper`:
|
||||
- Данные в `/usr/share/winehelper/`
|
||||
- Рабочая директория пользователя: `~/.local/share/winehelper/`
|
||||
- Иконки в `/usr/share/icons/...`
|
||||
|
||||
### Режим разработки
|
||||
При запуске из репозитория:
|
||||
- Рабочая директория: `~/test-winehelper/`
|
||||
- Данные берутся из текущей директории
|
||||
- Включается проверка синтаксиса скриптов
|
||||
|
||||
## Команды CLI
|
||||
|
||||
```bash
|
||||
# Установка
|
||||
winehelper install list # Список доступных программ
|
||||
winehelper -i <script_name> # Установить программу
|
||||
winehelper -i <script_name> /path # Установить из дистрибутива
|
||||
|
||||
# Управление префиксами
|
||||
winehelper create-prefix # Создать префикс
|
||||
winehelper remove-prefix [name] # Удалить префикс
|
||||
winehelper backup-prefix [name] # Создать бэкап (.whpack)
|
||||
winehelper restore-prefix <path> # Восстановить из бэкапа
|
||||
|
||||
# Wine tools
|
||||
winehelper winecfg # Настройки Wine
|
||||
winehelper winereg # Редактор реестра
|
||||
winehelper winefile # Файловый менеджер
|
||||
winehelper winetricks [component] # Winetricks
|
||||
winehelper <file.exe> # Запустить exe в префиксе
|
||||
|
||||
# Управление компонентами
|
||||
winehelper install-dxvk [version] # Установить DXVK
|
||||
winehelper install-vkd3d [version] # Установить VKD3D
|
||||
winehelper change-wine [version] # Сменить версию Wine
|
||||
|
||||
# Другое
|
||||
winehelper --debug <command> # Режим отладки
|
||||
winehelper --changelog # История изменений
|
||||
winehelper gui # Запустить GUI
|
||||
```
|
||||
|
||||
## Разработка
|
||||
|
||||
### Запуск в режиме разработки
|
||||
|
||||
```bash
|
||||
# GUI режим
|
||||
./winehelper gui
|
||||
|
||||
# CLI режим
|
||||
./winehelper install list
|
||||
./winehelper -i alta-expert
|
||||
```
|
||||
|
||||
### Добавление нового скрипта установки
|
||||
|
||||
1. Создать файл в `autoinstall/` (для автоматической установки) или `manualinstall/` (требуется дистрибутив)
|
||||
2. Добавить обязательные переменные: `PROG_NAME`, `WINEPREFIX`, `WH_WINE_USE`, `PROG_URL`
|
||||
3. Добавить комментарий `# info_ru:` с описанием
|
||||
4. Добавить иконку в `image/` (PNG, имя совпадает с переменной в create_desktop)
|
||||
5. Обновить `sha256sum.list` если требуется загрузка архивов
|
||||
|
||||
### Требования к коду
|
||||
|
||||
**Bash:**
|
||||
- Использовать `#!/usr/bin/env bash`
|
||||
- Проверять наличие переменных перед использованием
|
||||
- Использовать функции обработки ошибок (`fatal`, `print_error`)
|
||||
- Экранировать все пути с помощью кавычек
|
||||
|
||||
**Python:**
|
||||
- Импортировать из `winehelperqt` относительно
|
||||
- Использовать type hints где возможно
|
||||
- Документация на русском языке
|
||||
- Классы должны наследоваться от соответствующих QWidget
|
||||
|
||||
## Безопасность
|
||||
|
||||
- Скрипт запрещает запуск от root
|
||||
- Проверяет noexec для /home
|
||||
- Проверяет SHA256 хеши загружаемых файлов
|
||||
- Использует временные файлы с ограниченными правами (600)
|
||||
- Поддержка лицензионного соглашения перед установкой
|
||||
|
||||
## Особенности ALT Linux
|
||||
|
||||
- Требуется репозиторий x86_64-i586 для 32-битных зависимостей
|
||||
- Интеграция с RPM (версия определяется через `rpm -q winehelper`)
|
||||
- Использование apt-get для установки зависимостей
|
||||
- Desktop-файлы размещаются в `~/.local/share/applications/`
|
||||
|
||||
## Полезные ссылки
|
||||
|
||||
- Официальная документация: https://www.altlinux.org/Winehelper
|
||||
- Совместимость ПО (P10): https://www.basealt.ru/fileadmin/user_upload/compatibility/P10-view2.html
|
||||
- Совместимость ПО (P11): https://www.basealt.ru/fileadmin/user_upload/compatibility/P11-view2.html
|
||||
|
||||
## Сторонние компоненты
|
||||
|
||||
- Wine / Wine-Staging / Proton / wine-tkg
|
||||
- Wine-Mono, Wine-Gecko
|
||||
- DXVK, VKD3D
|
||||
- Winetricks
|
||||
- wine-cpcsp_proxy (для КриптоПро)
|
||||
@@ -1,5 +1,29 @@
|
||||
История изменений:
|
||||
|
||||
0.12.0
|
||||
* исправлена работа cpcsp_proxy при монтировании home с флагом noexec
|
||||
* обновлены все скрипты установки ВЭД и СТМ
|
||||
|
||||
0.11.0
|
||||
* исправлена работа WineHelper при монтировании home с флагом noexec
|
||||
* исправлена иконка графического режима Qt5
|
||||
* добавлена автоматическая архивация лога, если он больше 10 Мб
|
||||
* исправлена работа сертификатов для emias (спасибо lav@)
|
||||
|
||||
0.10.0
|
||||
* добавлены скрипты установок:
|
||||
- apropos-z
|
||||
- olimp
|
||||
* обновлены скрипты установок:
|
||||
- ctm-service
|
||||
- emias
|
||||
* добавлены версии wine:
|
||||
- wine-11.3.1-alt1-wow64
|
||||
- wine-10.18.1-alt2-i586
|
||||
- wine-9.0-cx-amd64
|
||||
* добавлена поддержка wine-cx (форк wine от crossover)
|
||||
* небольшие улучшения графического режим Qt5
|
||||
|
||||
0.9.0
|
||||
* добавлена поддержка wine-wow64 (не требует установку 32-битных зависимостей в систему)
|
||||
* добавлена поддержка wine-cpcsp_proxy для wine-wow64 (не требует установку 32-битного КриптоПро в систему)
|
||||
|
||||
@@ -48,19 +48,19 @@
|
||||
$ winehelper -i
|
||||
```
|
||||
|
||||
### Запуск установки приложений
|
||||
### Запуск установки приложений из списка скриптов
|
||||
|
||||
Скрипты установки разделены на два типа:
|
||||
|
||||
1. **Список программ с возможностью автоматической установки** — содержит полностью автоматизированные скрипты установок программ у которыx есть дистрибутивы (установщики) в свободном доступе.
|
||||
1. **Список программ с возможностью автоматической установки** — содержит полностью автоматизированные скрипты установок программ у которых есть дистрибутивы (установщики) в свободном доступе.
|
||||
Процесс полностью автоматизирован.
|
||||
Пример:
|
||||
```
|
||||
$ winehelper install spravki-bk
|
||||
$ winehelper install ksamu
|
||||
```
|
||||
или сокращенная команда:
|
||||
```
|
||||
$ winehelper -i spravki-bk
|
||||
$ winehelper -i ksamu
|
||||
```
|
||||
2. **Список программ с возможностью установки из существующего дистрибутива** — содержит скрипты установок для программ, которых нет в свободном доступе.
|
||||
Пример:
|
||||
@@ -72,7 +72,7 @@
|
||||
$ winehelper -i is-record-station "/путь/до/установочного_файла"
|
||||
```
|
||||
|
||||
### Процесс установки приложений
|
||||
### Процесс установки приложений из списка скриптов
|
||||
|
||||
При первом запуске **WineHelper** проверяются и при необходимости устанавливаются дополнительные зависимости (запрашиваются права root).
|
||||
|
||||
@@ -88,6 +88,27 @@
|
||||
|
||||
Если устанавливаемое приложение требует дополнительных действий от пользователя (ввод лицензии, настройка), это будет явно указано в терминале.
|
||||
|
||||
### Установка своего приложения
|
||||
|
||||
Для установки своего приложения (не из списка готовых скриптов) выполните следующие шаги:
|
||||
|
||||
**Этап 1: Создание префикса**
|
||||
Создайте новый префикс с использованием wine-wow64 (рекомендуется для новых приложений):
|
||||
```
|
||||
$ winehelper create-prefix
|
||||
```
|
||||
Выберите wine-wow64 версию и разрядность 64-bit.
|
||||
|
||||
> [!NOTE]
|
||||
> **Всегда сначала пробуйте wine-wow64**, это упрощает настройку и избавляет от 32-битных зависимостей.
|
||||
|
||||
**Этап 2: Запуск установщика**
|
||||
Запустите установочный файл в созданном префиксе:
|
||||
```
|
||||
$ winehelper "/путь/до/установочного_файла.exe"
|
||||
```
|
||||
Выберите префикс из списка (введите номер и нажмите Enter).
|
||||
|
||||
### Удаление префикса с приложением
|
||||
|
||||
Для удаления префикса выполните команду:
|
||||
@@ -101,14 +122,14 @@
|
||||
2 - Префикс2
|
||||
Выберите префикс (0-2):
|
||||
```
|
||||
Необходимо указать номер удаляемого префикса и нажать клавишу **Enter**.Далее потребуется подтвердить удаление префикса вводом **y**.
|
||||
Необходимо указать номер удаляемого префикса и нажать клавишу **Enter**. Далее потребуется подтвердить удаление префикса вводом **y**.
|
||||
|
||||
В результате будут удалены:
|
||||
- сам префикс;
|
||||
- установленное ПО;
|
||||
- desktop-файлы (ярлыки для установленного ПО в префиксе).
|
||||
|
||||
###
|
||||
### Cоздание резервной копии префикса
|
||||
|
||||
Команда создания резервной копии префикса:
|
||||
```
|
||||
@@ -138,7 +159,7 @@
|
||||
Восстановление префикса не требует подключение к интернету и происходит в автоматическом режиме:
|
||||
- Распаковка префикса.
|
||||
- Восстановление версии WINE.
|
||||
- Восстановление ярлыков (.desktop-файdлов).
|
||||
- Восстановление ярлыков (.desktop-файлов).
|
||||
|
||||
После завершения восстановления приложение будет доступно для запуска через меню или рабочий стол.
|
||||
|
||||
@@ -175,7 +196,7 @@ WineHelper предоставляет доступ к основным инст
|
||||
```
|
||||
Файл будет запущен в выбранном префиксе.
|
||||
|
||||
2. С помощью файлового менеджера WINE:'''
|
||||
2. С помощью файлового менеджера WINE:
|
||||
```
|
||||
$ winehelper winefile
|
||||
0 - Отмена
|
||||
@@ -239,7 +260,7 @@ WineHelper предоставляет доступ к основным инст
|
||||
<p><em>Вкладка "Ручная установка"</em></p>
|
||||
</div>
|
||||
|
||||
Так же на вкладке **Автоматическая установка** есть чекбокс с тестовыми версиями скриптов установки которые еще не прошли полную проверку на зависимости и совместимость, но уже можно попробовать в работе.
|
||||
Также на вкладке **Автоматическая установка** есть чекбокс с тестовыми версиями скриптов установки которые еще не прошли полную проверку на зависимости и совместимость, но уже можно попробовать в работе.
|
||||
|
||||
<div align="center">
|
||||
<img src="image/handbook/auto_install_test.png">
|
||||
@@ -324,9 +345,9 @@ WineHelper предоставляет доступ к основным инст
|
||||
#### Создание нового префикса
|
||||
|
||||
Нажав кнопку **«Создать новый префикс»**, вы откроете диалог, где можно задать:
|
||||
* **Имя префикса**.
|
||||
* **Имя нового префикса**.
|
||||
* **Разрядность** (32-bit или 64-bit).
|
||||
* **Наполнение** (чистый префикс или с рекомендуемыми библиотеками).
|
||||
* **Шаблон префикса** (чистый префикс или с рекомендуемыми библиотеками).
|
||||
* **Версию Wine** из доступного списка.
|
||||
|
||||
<div align="center">
|
||||
@@ -334,22 +355,45 @@ WineHelper предоставляет доступ к основным инст
|
||||
<p><em>Диалог создания нового префикса</em></p>
|
||||
</div>
|
||||
|
||||
#### Установка своего приложения
|
||||
|
||||
Для установки своего приложения (не из списка готовых скриптов) выполните следующие шаги:
|
||||
|
||||
**Этап 1: Создание префикса**
|
||||
1. Вкладка **«Менеджер префиксов»**
|
||||
2. Нажмите **«Создать новый префикс»**
|
||||
3. Задайте имя префикса
|
||||
4. Выберите **wine-wow64** версию (рекомендуется)
|
||||
5. Выберите разрядность **64-bit**
|
||||
|
||||
> [!NOTE]
|
||||
> **Всегда сначала пробуйте wine-wow64**, это упрощает настройку и избавляет от 32-битных зависимостей.
|
||||
|
||||
**Этап 2: Установка приложения**
|
||||
1. Выберите созданный префикс из выпадающего списка
|
||||
2. Нажмите кнопку **«Запустить файл в префиксе»**, выберите файл для установки
|
||||
3. Нажмите кнопку **«Создать ярлык для приложения в префиксе»**, выберите установленный файл в префиксе
|
||||
|
||||
#### Управление существующим префиксом
|
||||
|
||||
Выбрав префикс из выпадающего списка, вы получаете доступ к панели управления, которая позволяет:
|
||||
|
||||
* **Управлять компонентами**:
|
||||
* **Менеджер компонентов (Winetricks)**: Удобный интерфейс для установки и переустановки библиотек, шрифтов и настроек.
|
||||
* **Запускать стандартные утилиты Wine**:
|
||||
* `Редактор настроек (winecfg)`
|
||||
* `Редактор реестра (regedit)`
|
||||
* `Удаление программ (uninstaller)`
|
||||
* `Командная строка (cmd)`
|
||||
* `Файловый менеджер (winefile)`
|
||||
* **Управлять компонентами**:
|
||||
* **Менеджер компонентов (Winetricks)**: Удобный интерфейс для установки и переустановки библиотек, шрифтов и настроек.
|
||||
* **Управление Wine**: Смена версии Wine для выбранного префикса. Диалог выбора позволяет как выбрать уже установленную версию, так и скачать новую из списков, сгруппированных по архитектуре.
|
||||
* `Панель управления (control)`
|
||||
* **Управлять версиями Wine**:
|
||||
* **Управление версией Wine**: Смена версии Wine для выбранного префикса. Диалог выбора позволяет как выбрать уже установленную версию, так и скачать новую из списков, сгруппированных по архитектуре.
|
||||
* **Управлять дополнительными настройками**:
|
||||
* **Управление DXVK/VKD3D**: Установка или удаление конкретных версий DXVK и vkd3d-proton.
|
||||
* **Ассоциации файлов**: Настройка открытия определенных типов файлов (например, `.pdf`, `.docx`) нативными приложениями Linux, чтобы избежать их открытия внутри Wine.
|
||||
* **Включать/выключать ESync и FSync**.
|
||||
* **Включать/выключать ESync и FSync**: Включение/выключение методов оптимизации синхронизации потоков в Windows-приложениях при запуске через Wine.
|
||||
* **Включать/выключать CPCSP_PROXY**: Поддержка КриптоПро CSP (электронная подпись). Включайте только для приложений, требующих ЭП.
|
||||
* **Устанавливать приложения**: Установить любой `.exe` или `.msi` файл напрямую в выбранный префикс.
|
||||
* **Создавать ярлыки**: Создать ярлык для любого исполняемого файла внутри префикса.
|
||||
* **Удалять префикс** или **создавать из него шаблон**.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# info_ru: Пакет программ для таможенного оформления. Включает программы Такса и ТамДок в комплектациях Эксперт, а также программу Заполнитель.
|
||||
########################################################################
|
||||
export PROG_URL="https://www.alta.ru"
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="alta"
|
||||
export PROG_NAME="Альта-Эксперт"
|
||||
export BASE_PFX="alta_pfx_x86_v01"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# info_ru: Таможенное оформление товаров
|
||||
########################################################################
|
||||
export PROG_URL="https://www.alta.ru"
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="alta"
|
||||
export PROG_NAME="Альта-ГТД"
|
||||
export PROG_ICON="altagtd"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# info_ru: Пакет программ для таможенного оформления. Включает программы Альта-ГТД, Такса и ТамДок в комплектациях Максимум, а также программу Заполнитель.
|
||||
########################################################################
|
||||
export PROG_URL="https://www.alta.ru"
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="alta"
|
||||
export PROG_NAME="Альта-Максимум"
|
||||
export BASE_PFX="alta_pfx_x86_v01"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# info_ru: Таможенное законодательство
|
||||
########################################################################
|
||||
export PROG_URL="https://www.alta.ru"
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="alta"
|
||||
export PROG_NAME="Таможенные документы"
|
||||
export PROG_ICON="altatamdoc"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# info_ru: Расчет контракта
|
||||
########################################################################
|
||||
export PROG_URL="https://www.alta.ru"
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="alta"
|
||||
export PROG_NAME="Такса"
|
||||
export PROG_ICON="altatax"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# info_ru: Автоматизация импорта данных
|
||||
########################################################################
|
||||
export PROG_URL="https://www.alta.ru"
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="alta"
|
||||
export PROG_NAME="Заполнитель"
|
||||
export PROG_ICON="altazap"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
export PROG_URL="https://www.ctm.ru"
|
||||
export PROG_NAME="СТМ-Финансы"
|
||||
export PROG_ICON="ctmfinance"
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="ved_ctm"
|
||||
export BASE_PFX="ved_ctm_pfx_x86_v05"
|
||||
export WINEARCH="win32"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
export PROG_URL="https://www.ctm.ru"
|
||||
export PROG_NAME="СТМ-Журналы"
|
||||
export PROG_ICON="ctmjournals"
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="ved_ctm"
|
||||
export BASE_PFX="ved_ctm_pfx_x86_v05"
|
||||
export WINEARCH="win32"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
export PROG_URL="https://www.ctm.ru"
|
||||
export PROG_NAME="Монитор ЭД"
|
||||
export PROG_ICON="ctmmonitor"
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="ved_ctm"
|
||||
export BASE_PFX="ved_ctm_pfx_x86_v05"
|
||||
export WINEARCH="win32"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
export PROG_URL="https://www.ctm.ru"
|
||||
export PROG_NAME="СТМ-Отчет"
|
||||
export PROG_ICON="ctmreport"
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="ved_ctm"
|
||||
export BASE_PFX="ved_ctm_pfx_x86_v05"
|
||||
export WINEARCH="win32"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
export PROG_URL="https://www.ctm.ru"
|
||||
export PROG_NAME="СТМ-Сервис"
|
||||
export PROG_ICON="ctmservice"
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="ved_ctm"
|
||||
export BASE_PFX="ved_ctm_pfx_x86_v05"
|
||||
export WINEARCH="win32"
|
||||
@@ -18,7 +18,7 @@ get_and_set_reg_file --add 'Software\CTM\CTMSETUP' 'SkipKBCheck' 'REG_DWORD' "1"
|
||||
|
||||
AUTOINSTALL_EXE="${WH_TMP_DIR}/setup_cs.exe"
|
||||
AUTOINSTALL_UNPACK="${WH_TMP_DIR}/setup_cs"
|
||||
AUTOINSTALL_SETUP="${WH_TMP_DIR}/setup_cs/setup.exe"
|
||||
AUTOINSTALL_SETUP="${WH_TMP_DIR}/setup_cs/Setup.exe"
|
||||
|
||||
if try_download "https://files.ctm.ru/CSERVICE/SFX/setup_cs.exe" "${AUTOINSTALL_EXE}" ; then
|
||||
unpack "${AUTOINSTALL_EXE}" "${AUTOINSTALL_UNPACK}"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# info_ru: Сервис обновления ПО: "R-Атлас", "R-Инфо" и "R-Тариф".
|
||||
########################################################################
|
||||
export PROG_URL="https://daobit.ru"
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_i586" # wine-9.0.9-alt1-i586"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586" # wine-9.0.9-alt1-i586"
|
||||
export WINEPREFIX="daobit"
|
||||
export PROG_NAME="Сервис обновления ПО"
|
||||
export PROG_ICON="daobitcservice"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# info_ru: Построение маршрутов железнодорожных грузоперевозок
|
||||
########################################################################
|
||||
export PROG_URL="https://daobit.ru"
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_i586" # wine-9.0.9-alt1-i586"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586" # wine-9.0.9-alt1-i586"
|
||||
export WINEPREFIX="daobit"
|
||||
export PROG_NAME="R-Атлас"
|
||||
export PROG_ICON="daobitratlas"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# info_ru: Специализированная информация для организации железнодорожных перевозок
|
||||
########################################################################
|
||||
export PROG_URL="https://daobit.ru"
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_i586" # wine-9.0.9-alt1-i586"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586" # wine-9.0.9-alt1-i586"
|
||||
export WINEPREFIX="daobit"
|
||||
export PROG_NAME="R-Инфо"
|
||||
export PROG_ICON="daobitrinfo"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# info_ru: Расчёт размера провозной платы и дополнительных сборов за грузовые перевозки по железной дороге
|
||||
########################################################################
|
||||
export PROG_URL="https://daobit.ru"
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_i586" # wine-9.0.9-alt1-i586"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586" # wine-9.0.9-alt1-i586"
|
||||
export WINEPREFIX="daobit"
|
||||
export PROG_NAME="R-Тариф"
|
||||
export PROG_ICON="daobitrtariff"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
export PROG_URL="https://www.keepsoft.ru"
|
||||
export PROG_NAME="Конструктор тестов"
|
||||
export AUTOINSTALL_EXE="${WH_TMP_DIR}/simulator4.exe"
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="keepsoft_similator4"
|
||||
export BASE_PFX="defpfx_x86_v01"
|
||||
export WINEARCH="win32"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
export PROG_URL="https://ctm.ru"
|
||||
export PROG_NAME="ВЭД-Алфавит"
|
||||
export PROG_ICON="vedalf"
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="ved_ctm"
|
||||
export BASE_PFX="ved_ctm_pfx_x86_v05"
|
||||
export WINEARCH="win32"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
export PROG_URL="https://ctm.ru"
|
||||
export PROG_NAME="ВЭД-Контроль"
|
||||
export PROG_ICON="vedcontrol"
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="ved_ctm"
|
||||
export BASE_PFX="ved_ctm_pfx_x86_v05"
|
||||
export WINEARCH="win32"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
export PROG_URL="https://ctm.ru"
|
||||
export PROG_NAME="ВЭД-Декларант"
|
||||
export PROG_ICON="veddcl"
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="ved_ctm"
|
||||
export BASE_PFX="ved_ctm_pfx_x86_v05"
|
||||
export WINEARCH="win32"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
export PROG_URL="https://ctm.ru"
|
||||
export PROG_NAME="ВЭД-Инфо"
|
||||
export PROG_ICON="vedinfo"
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="ved_ctm"
|
||||
export BASE_PFX="ved_ctm_pfx_x86_v05"
|
||||
export WINEARCH="win32"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
export PROG_URL="https://ctm.ru"
|
||||
export PROG_NAME="ВЭД-Платежи"
|
||||
export PROG_ICON="vedpayments"
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="ved_ctm"
|
||||
export BASE_PFX="ved_ctm_pfx_x86_v05"
|
||||
export WINEARCH="win32"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
export PROG_URL="https://ctm.ru"
|
||||
export PROG_NAME="ВЭД-Склад"
|
||||
export PROG_ICON="vedsts"
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="ved_ctm"
|
||||
export BASE_PFX="ved_ctm_pfx_x86_v05"
|
||||
export WINEARCH="win32"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# info_ru: Автоматизация установки МИС Витакарта (AIS LPU Client).
|
||||
########################################################################
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_i586"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="ais-lpu-client"
|
||||
export PROG_NAME="AIS LPU Client"
|
||||
export PROG_ICON="ais_lpu_client"
|
||||
|
||||
|
Before Width: | Height: | Size: 243 KiB After Width: | Height: | Size: 290 KiB |
|
Before Width: | Height: | Size: 204 KiB After Width: | Height: | Size: 284 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 199 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 217 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 234 KiB After Width: | Height: | Size: 256 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 236 KiB After Width: | Height: | Size: 232 KiB |
|
Before Width: | Height: | Size: 336 KiB After Width: | Height: | Size: 365 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 258 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 194 KiB After Width: | Height: | Size: 245 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 208 KiB |
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# info_ru: Специализированное программное обеспечение «Автономная станция прослушивания» предназначено для загрузки и прослушивания электронных материалов итогового собеседования.
|
||||
########################################################################
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_i586"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="desktopaudiostation"
|
||||
export PROG_NAME="АС Прослушивания"
|
||||
export PROG_ICON="desktopaudiostation"
|
||||
|
||||
@@ -2,16 +2,14 @@
|
||||
# info_ru: Установщик "ЕМИАС" - Единая медицинская информационно-аналитическая система
|
||||
########################################################################
|
||||
export PROG_URL="https://emias.info/"
|
||||
export WH_WINE_USE="wine-10.18.1-alt1-wow64"
|
||||
export WH_WINE_USE="wine-11.4.1-emias-alt2-wow64"
|
||||
export WINEPREFIX="emias"
|
||||
export PROG_NAME="КИС ЕМИАС"
|
||||
export BASE_PFX="emias_pfx_x64_v01"
|
||||
export BASE_PFX="none"
|
||||
export WINEARCH="win64"
|
||||
export INSTALL_DLL="crypt32"
|
||||
export WH_WINDOWS_VER="7"
|
||||
export WH_USE_CPCSP_PROXY="1"
|
||||
export STAGING_SHARED_MEMORY="1"
|
||||
export WINE_LARGE_ADDRESS_AWARE="1"
|
||||
export CPCSP_PROXY_WOW64_VER="0.7.7-alt1.1-em-wow64"
|
||||
|
||||
if [[ -f "$2" ]] \
|
||||
&& [[ $(basename "$2") =~ (kis\.exe|kis_z\.exe) ]]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# info_ru: Специализированное программное обеспечение «Автономная станция записи» предназначено для записи ответов участников итогового собеседования.
|
||||
########################################################################
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_i586"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="isrecordingstation"
|
||||
export PROG_NAME="АС Записи"
|
||||
export PROG_ICON="isrecordingstation"
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
# info_ru: "КИС Олимп" - программно-аналитический комплекс, состоящий из самостоятельных подключаемых модулей.
|
||||
########################################################################
|
||||
export PROG_URL="https://olimp.best/"
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_i586"
|
||||
export WH_WINE_USE="wine-10.18.1-alt2-i586"
|
||||
export WINEPREFIX="olimp"
|
||||
export PROG_NAME="КИС Олимп"
|
||||
export BASE_PFX="olimp_pfx_x86_v01"
|
||||
export BASE_PFX="olimp_pfx_x86_v02"
|
||||
export WINEARCH="win32"
|
||||
export WH_WINDOWS_VER="7"
|
||||
export INSTALL_DLL="jet40 dotnet45 corefonts lucida opensymbol tahoma cjkfonts mdac28 msxml4 native_oleaut32 vcrun2005"
|
||||
export WINEDLLOVERRIDES="d2d1=n"
|
||||
export WH_MAIN_DECORATED="0"
|
||||
export FREETYPE_PROPERTIES="truetype:interpreter-version=35"
|
||||
|
||||
if [[ -f "$2" ]] \
|
||||
&& [[ $(basename "$2") =~ (Loader\.exe) ]]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# info_ru: Это мощный, современный инструмент быстрой и качественной разработки АСУТП
|
||||
########################################################################
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_i586" # wine-9.0.9-alt1-i586"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586" # wine-9.0.9-alt1-i586"
|
||||
export WINEPREFIX="scada-krug-2000"
|
||||
export PROG_NAME="SCADA КРУГ 2000"
|
||||
export PROG_ICON="krugtaskmanager"
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
##### WINE WOW64 #####
|
||||
e9a8b79dfe12cad1304dca573d73aefe109e3d69b6fff739c8dbb8b2d3c271c6 wine-10.12.1-alt1-wow64.tar.xz
|
||||
a862761c432f8619caf8100589678a1cbb523787259120ccd4388089d81e3b17 wine-10.18.1-tflex-alt2-wow64.tar.xz
|
||||
8b9a6548eb5df921514cd569607e7f845acd18a3a6cf56f1f724b38bc9a94e43 wine-11.6.1-scadoffice-alt5-wow64.tar.xz
|
||||
a176e2ee3d60f691538b3c463047ff5eabd0ba2e37c0158783d79e9fa68caa2f wine-11.4.1-emias-alt2-wow64.tar.xz
|
||||
3c7c394658ad993968532e20ae9b591c1a0ec5b7495c8450de09bd4f8947462e wine-11.3.1-alt1-wow64.tar.xz
|
||||
349c707148a23e667970309248bbbf97b4e2a0db59c548e73d9fe9c3f585872c wine-10.18.1-alt1-wow64.tar.xz
|
||||
a862761c432f8619caf8100589678a1cbb523787259120ccd4388089d81e3b17 wine-10.18.1-tflex-alt2-wow64.tar.xz
|
||||
e9a8b79dfe12cad1304dca573d73aefe109e3d69b6fff739c8dbb8b2d3c271c6 wine-10.12.1-alt1-wow64.tar.xz
|
||||
|
||||
##### WINE AMD64 #####
|
||||
009c95bfe2df3f9264c9c5092f3e30ea7a168dd7869046058a718a70739602d4 wine_wh_tflex_10-9_amd64.tar.xz
|
||||
71d6fdfd23d7988471d345c68e81699b8af931352b03dd216424b398f63c7b12 wine_x_tkg_10-0_amd64.tar.xz
|
||||
e0a84bb4908c3927954d7eef6b8ac7212e442b8c107d000c6890fec340f96183 wine-9.0.14-alt1-amd64.tar.xz
|
||||
fd209c95a3aa507002374417365b3d1119dbe3a0a6d223034171221a2b187b66 wine-9.0-cx-amd64.tar.xz
|
||||
f1bf1261550ca2928cefacdb724926d3d6d103433d0ff6882ee9783a50d8f4e4 wine-8.8-staging-amd64.tar.xz
|
||||
61bec1230b37b8fcc69fd45f848b44fd88cc41fcdd5dc3080336d7da63660f40 wine-7.16.1-alt1-amd64.tar.xz
|
||||
fd209c95a3aa507002374417365b3d1119dbe3a0a6d223034171221a2b187b66 wine-9.0-cx-amd64.tar.xz
|
||||
|
||||
##### WINE I586 #####
|
||||
1385323e059c376299a83088ada85e625c170337ba07e5307df75961726d8eee wine-10.18.1-alt2-i586.tar.xz
|
||||
49ce3e1152f01c1f24ca148092a66f6eccd68cf4ee35d0cb04900120f45dd864 wine-10.12.1-alt1-i586.tar.xz
|
||||
3571c40a787f0386e0160b5d471b1bf11fa2a235b497156cd38198b90bcb4a9f wine_x_tkg_10-0_i586.tar.xz
|
||||
fb7fdfde96de10a1b3b051bdf2727b6a7c1768b878483726454dd6726e9e0193 wine-9.0.14-alt1-i586-spravkibk.tar.xz
|
||||
6f86d2220b65b709bf88c6f829a4998de3b929cc2091cd1333a51c32e1491b79 wine-9.0.9-alt1-i586.tar.xz
|
||||
6fea17fd131f57c2ebf7ca4c60d3c5a9e819afe16e5d0b77ecb750da99ae0e38 wine-7.16.1-alt1-i586.tar.xz
|
||||
|
||||
##### DXVK #####
|
||||
d1a86ca53ab7e156e1252d3cf7eed3c66fe0651699cfc9c4ba152024261d8258 dxvk-1.10.1-967.tar.xz
|
||||
98411effbc1b3611b11c4ca3af2ad29400b08e1c8c380791d6351600a06fb3c7 dxvk-1.10.3-28.tar.xz
|
||||
061568b51ce99d285ef2d0f1f31aab65b3e6fd34747073859c5a719bec2a7afd dxvk-1.6.1.tar.xz
|
||||
37ed4a8cd4c2bff3abbf830e6d602c2e0a971eda7c69b3f45834539bc84b1431 dxvk-2.0-116.tar.xz
|
||||
@@ -114,9 +118,9 @@ b3f56f58b5b764aba564c8b8c69eb12425467d57bf2a6457edff08842af03573 vkd3d-proton-1
|
||||
edf16d2b37bc77d121d5d81b06b60d3f694e0060c6606e729ceab30de3d27466 vkd3d-proton-sarek-2.6.0.tar.xz
|
||||
|
||||
##### CPCSP_PROXY #####
|
||||
3153088ff28d6af415c3504a421e5382554dbb305f38d5a87dd84df90d680421 wine-cpcsp_proxy-0.6.1-alt1.tar.xz
|
||||
fbe72bfe3c2c307363782ce2d37589e142b44111f269657b0e516a6b2d6ab8a9 wine-cpcsp_proxy-0.7.3-alt5-wow64.tar.xz
|
||||
5a0c582f662ac501865f569124c153b43cded72eb98a001eaa7ebd5b2168f1a2 wine-cpcsp_proxy-0.7.7-alt1.1-em-wow64.tar.xz
|
||||
6f43427148c6f424cd49a406328669e4ec4f21df7bc86f1a6aad2b5c16b50e39 wine-cpcsp_proxy-0.7.7-alt1-wow64.tar.xz
|
||||
3153088ff28d6af415c3504a421e5382554dbb305f38d5a87dd84df90d680421 wine-cpcsp_proxy-0.6.1-alt1.tar.xz
|
||||
|
||||
##### FONTS #####
|
||||
405bfe3b7c7f80034837c05656535053305727ee4bf1d993521b67b71d08ebc6 extra_fonts_v01.tar.xz
|
||||
@@ -183,10 +187,6 @@ dfb44ce5e5af7dba1686932c63d6b05e5dd6919a21c78130a7d1d0271b93958e audiorecstatio
|
||||
# create with wine_x_tkg_10-0_i586 (universal user: xuser)
|
||||
# winetricks arial dotnet7 dotnetdesktop7 renderer=gdi
|
||||
|
||||
8c6312f2e4e846a98ca4a87fc90ee1917eb28d4caaddde040fb4d2dd05f8c0fe scadaoffice_pfx_x64_v05.tar.xz
|
||||
# create with wine_x_tkg_10-0_amd64 (universal user: xuser)
|
||||
# winetricks dotnet48 gdiplus vcrun6sp6 vcrun2005 vcrun2019 d3dx11_42 d3dx11_43 d3dx9 d3dcompiler_42 d3dcompiler_43 d3dcompiler_46 d3dcompiler_47 richtx32 riched30 riched20 msxml6 dotnet20
|
||||
|
||||
ef7e8f1ba785d48e4ea287feed5b79bd630d423e59efadb43da9653adefef218 ais-lpu-client_pfx_x86_v01.tar.xz
|
||||
# create with wine_x_tkg_10-0_i586 (universal user: xuser)
|
||||
# winetricks vcrun2005 vcrun2008 dotnet20sp2 dotnet40 mfc42 7zip
|
||||
@@ -216,11 +216,15 @@ d16c300b5451e4fad6eeebeca0949367a7bdf1cb1878fc51f0f9dee33744d3a7 emias_pfx_x64_
|
||||
# create with wine-10.18.1-alt1-wow64 (universal user: xuser)
|
||||
# winetricks crypt32
|
||||
|
||||
66a31bf72aac7354956c0ce8ddb9a2858b291bb9c8b94229b0820b0119ced97f olimp_pfx_x86_v01.tar.xz
|
||||
# create with wine_x_tkg_10-0_i586 (universal user: xuser)
|
||||
cf7f6284fc46d9a0be9756721d04e9f8842a481f805829711d4340fed5e67e69 olimp_pfx_x86_v02.tar.xz
|
||||
# create with wine-10.18.1-alt2-i586
|
||||
# winetricks jet40 dotnet45 corefonts lucida opensymbol tahoma cjkfonts mdac28 msxml4 native_oleaut32 vcrun2005
|
||||
# + psqlodbc_x86.msi installed from script
|
||||
|
||||
4fc8ba95190d7d2cd05f60f29fe6af801f99bf046ab75bccd82bf48a600d5fc4 scadoffice_pfx_x64_v06.tar.xz
|
||||
# create with wine-10.18.1-alt1-wow64 (universal user: xuser)
|
||||
# winetricks msxml6 gdiplus vcrun6sp6 vcrun2005 ucrtbase2019 vcrun2019 d3dx9 d3dx11_43 d3dx11_42 d3dcompiler_42 d3dcompiler_43 d3dcompiler_46 d3dcompiler_47 dotnet40 dotnet48 riched20 riched30 msls31 richtx32
|
||||
|
||||
##### ADDONS #####
|
||||
# addons with ODBC, SSH, *.reg
|
||||
0f4ef434df07bc338ae308af44330590eaa1d9c94b64850514e55b960642d0eb scadoffice_addons_v02.tar.xz
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# info_ru: Программное обеспечение для 3D-графики, предназначенное для проектирования освещения. (Версия 4.4.0.2)
|
||||
########################################################################
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_i586"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="dialux"
|
||||
export PROG_NAME="DIALux"
|
||||
export PROG_ICON="dialux"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# info_ru: Удобная программа для создания двухмерных схем и чертежей. Основной акцент сделан на проектировании систем тепло-, водо- и газоснабжения, водоочистки, автоматизированных систем управления технологическими процессами (АСУТП), а также планов эвакуации при пожаре и авариях.
|
||||
########################################################################
|
||||
export PROG_URL="https://easymnemo.com"
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_i586"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="easymnemo"
|
||||
export PROG_NAME="Easymnemo"
|
||||
export PROG_ICON="easymnemo"
|
||||
|
||||
@@ -6,42 +6,114 @@ export WH_WINE_USE="wine-10.18.1-alt1-wow64"
|
||||
export WINEPREFIX="nalog"
|
||||
export PROG_NAME="Налогоплательщик ЮЛ"
|
||||
export PROG_ICON="npul"
|
||||
export BASE_PFX="nalog_pfx_x64_v01"
|
||||
export BASE_PFX="none"
|
||||
export WINEARCH="win64"
|
||||
export WH_WINDOWS_VER="10"
|
||||
export WH_XDG_OPEN="doc"
|
||||
export INSTALL_DLL="msxml4 msxml6 corefonts wsh57 gdiplus"
|
||||
export INSTALL_DLL="msxml4 msxml6 corefonts wsh57 gdiplus vb6run"
|
||||
|
||||
try_get_page "https://data.nalog.ru/rn77/program/5961229/"
|
||||
VER_MSI=$(read_page | grep -oP 'NalogUL\d+\.msi' | tail -n 1 | grep -oP '\d+')
|
||||
VER_MSI_URL="${VER_MSI:0:1}.${VER_MSI:1}" # 492 → 4.92
|
||||
VER_EXE=$(read_page | grep -oP 'NalogUL\d+\.exe' | tail -n 1 | grep -oP '\d+')
|
||||
VER_EXE_URL="${VER_EXE:0:1}.${VER_EXE:1:2}.${VER_EXE:3}" # 4924 → 4.92.4
|
||||
HTML_CONTENT=$(read_page)
|
||||
|
||||
AUTOINSTALL_MSI="${WH_TMP_DIR}/NalogUL${VER_MSI}.msi"
|
||||
AUTOINSTALL_EXE="${WH_TMP_DIR}/NalogUL${VER_EXE}.exe"
|
||||
# Извлекаем все версии одним проходом (и 3, и 4 цифры, и MSI, и EXE)
|
||||
declare -A VERSIONS
|
||||
while IFS= read -r match; do
|
||||
if [[ -n "$match" ]]; then
|
||||
ver=$(echo "$match" | grep -oP '\d+')
|
||||
ext="${match##*.}"
|
||||
digits="${#ver}"
|
||||
VERSIONS["${digits}_${ext}"]="$ver"
|
||||
fi
|
||||
done < <(echo "$HTML_CONTENT" | grep -oP 'NalogUL\d{3,4}\.(msi|exe)')
|
||||
|
||||
prepair_wine
|
||||
if try_download "https://data.nalog.ru/files/nalul/${VER_MSI_URL}/NalogUL${VER_MSI}.msi" "${AUTOINSTALL_MSI}" ; then
|
||||
wine_run_install "${AUTOINSTALL_MSI}" /quiet INSTALLDIR="c:\npul"
|
||||
try_remove_file "${AUTOINSTALL_MSI}"
|
||||
|
||||
PRINT_INSTALLER="$(find "$DRIVE_C/npul/INPUTDOC" -name "Печать НД с PDF417*.msi")"
|
||||
wine_run_install "${PRINT_INSTALLER}" "/q"
|
||||
|
||||
# Процедуры конвертации, индексация данных. По завершении процедур программа сама закроется. Признаком успешного завершения будет отсутствие в папке с программой файла setupmarker.ini. Если не принять процедуру, .exe файл обновления не установиться.
|
||||
wine_run_install "$DRIVE_C/npul/INPUTDOC/inputdoc.exe" go.ini
|
||||
#TODO: Сделать запуск процедуры в тихом режиме.
|
||||
# Версия программы (3 цифры, приоритет MSI)
|
||||
if [[ -n "${VERSIONS[3_msi]}" ]]; then
|
||||
PROG_VER="${VERSIONS[3_msi]}"
|
||||
PROG_EXT="msi"
|
||||
elif [[ -n "${VERSIONS[3_exe]}" ]]; then
|
||||
PROG_VER="${VERSIONS[3_exe]}"
|
||||
PROG_EXT="exe"
|
||||
else
|
||||
print_error "Не найдена установочная версия программы"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$VER_EXE" ]] \
|
||||
&& try_download "https://data.nalog.ru/files/nalul/${VER_EXE_URL}/NalogUL${VER_EXE}.exe" "${AUTOINSTALL_EXE}"
|
||||
then
|
||||
wine_run_install "${AUTOINSTALL_EXE}" "/SILENT"
|
||||
try_remove_file "${AUTOINSTALL_EXE}"
|
||||
# Версия обновления (4 цифры, приоритет EXE)
|
||||
if [[ -n "${VERSIONS[4_exe]}" ]]; then
|
||||
UPDATE_VER="${VERSIONS[4_exe]}"
|
||||
UPDATE_EXT="exe"
|
||||
elif [[ -n "${VERSIONS[4_msi]}" ]]; then
|
||||
UPDATE_VER="${VERSIONS[4_msi]}"
|
||||
UPDATE_EXT="msi"
|
||||
else
|
||||
UPDATE_VER=""
|
||||
UPDATE_EXT=""
|
||||
fi
|
||||
|
||||
PRINT_INSTALLER="$(find "$DRIVE_C/npul/INPUTDOC" -name "Печать НД с PDF417*.msi" | tail -n 1)"
|
||||
# Формируем URL пути
|
||||
PROG_URL_PATH="${PROG_VER:0:1}.${PROG_VER:1}"
|
||||
AUTOINSTALL_PROG="${WH_TMP_DIR}/NalogUL${PROG_VER}.${PROG_EXT}"
|
||||
|
||||
if [[ -n "$UPDATE_VER" ]]; then
|
||||
UPDATE_URL_PATH="${UPDATE_VER:0:1}.${UPDATE_VER:1:2}.${UPDATE_VER:3}"
|
||||
AUTOINSTALL_UPDATE="${WH_TMP_DIR}/NalogUL${UPDATE_VER}.${UPDATE_EXT}"
|
||||
fi
|
||||
|
||||
prepair_wine
|
||||
|
||||
# Устанавливаем основную программу
|
||||
if [[ "$PROG_EXT" == "msi" ]]; then
|
||||
if try_download "https://data.nalog.ru/files/nalul/${PROG_URL_PATH}/NalogUL${PROG_VER}.msi" "${AUTOINSTALL_PROG}"; then
|
||||
wine_run_install "${AUTOINSTALL_PROG}" /quiet /norestart INSTALLDIR="c:\npul"
|
||||
try_remove_file "${AUTOINSTALL_PROG}"
|
||||
else
|
||||
print_error "Не удалось скачать установочный файл MSI"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if try_download "https://data.nalog.ru/files/nalul/${PROG_URL_PATH}/NalogUL${PROG_VER}.exe" "${AUTOINSTALL_PROG}"; then
|
||||
wine_run_install "${AUTOINSTALL_PROG}" /VERYSILENT /NORESTART /DIR="c:\npul"
|
||||
try_remove_file "${AUTOINSTALL_PROG}"
|
||||
else
|
||||
print_error "Не удалось скачать установочный файл EXE"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Устанавливаем печать PDF417
|
||||
PRINT_INSTALLER="$(find "$DRIVE_C/npul/INPUTDOC" -name "Печать НД с PDF417*.msi" | tail -n 1)"
|
||||
if [[ -n "$PRINT_INSTALLER" ]]; then
|
||||
wine_run_install "${PRINT_INSTALLER}" "/q"
|
||||
fi
|
||||
|
||||
# Процедуры конвертации, индексация данных
|
||||
wine_run_install "$DRIVE_C/npul/INPUTDOC/inputdoc.exe" go.ini
|
||||
|
||||
# Устанавливаем обновление (если есть)
|
||||
if [[ -n "$UPDATE_VER" ]]; then
|
||||
if [[ "$UPDATE_EXT" == "msi" ]]; then
|
||||
if try_download "https://data.nalog.ru/files/nalul/${UPDATE_URL_PATH}/NalogUL${UPDATE_VER}.msi" "${AUTOINSTALL_UPDATE}"; then
|
||||
wine_run_install "${AUTOINSTALL_UPDATE}" /quiet /norestart
|
||||
try_remove_file "${AUTOINSTALL_UPDATE}"
|
||||
|
||||
# После обновления может появиться новая версия печати
|
||||
PRINT_INSTALLER="$(find "$DRIVE_C/npul/INPUTDOC" -name "Печать НД с PDF417*.msi" | tail -n 1)"
|
||||
if [[ -n "$PRINT_INSTALLER" ]]; then
|
||||
wine_run_install "${PRINT_INSTALLER}" /quiet /norestart
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if try_download "https://data.nalog.ru/files/nalul/${UPDATE_URL_PATH}/NalogUL${UPDATE_VER}.exe" "${AUTOINSTALL_UPDATE}"; then
|
||||
wine_run_install "${AUTOINSTALL_UPDATE}" /VERYSILENT /NORESTART
|
||||
try_remove_file "${AUTOINSTALL_UPDATE}"
|
||||
|
||||
# После обновления может появиться новая версия печати
|
||||
PRINT_INSTALLER="$(find "$DRIVE_C/npul/INPUTDOC" -name "Печать НД с PDF417*.msi" | tail -n 1)"
|
||||
if [[ -n "$PRINT_INSTALLER" ]]; then
|
||||
wine_run_install "${PRINT_INSTALLER}" /quiet /norestart
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
else
|
||||
print_info "Дополнительных обновлений не требуется."
|
||||
fi
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# info_ru: Цифровая платформа для обучения и оценки специалистов нефтегазовой отрасли.
|
||||
########################################################################
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_i586"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export PROG_NAME="РТСИМ.Карьера"
|
||||
export WINEPREFIX="rtsim"
|
||||
export PROG_ICON="rtsim"
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
# info_ru: SCAD Office — это программный комплекс для расчёта строительных конструкций.
|
||||
########################################################################
|
||||
export PROG_URL="https://scadoffice.ru"
|
||||
export WH_WINE_USE="wine-8.8-staging-amd64"
|
||||
export WH_WINE_USE="wine-11.6.1-scadoffice-alt5-wow64"
|
||||
export WINEPREFIX="scadoffice"
|
||||
export PROG_NAME="SCAD Office"
|
||||
export PROG_ICON="scadoffice"
|
||||
export BASE_PFX="none"
|
||||
export WH_WINDOWS_VER="10"
|
||||
export BASE_PFX="scadoffice_pfx_x64_v06"
|
||||
export WH_WINDOWS_VER="7"
|
||||
export WINEARCH="win64"
|
||||
export INSTALL_DLL="dotnet48 gdiplus vcrun6sp6 vcrun2005 vcrun2019 d3dx11_42 d3dx11_43 d3dx9 d3dcompiler_42 d3dcompiler_43 d3dcompiler_46 d3dcompiler_47 richtx32 riched30 riched20 msxml6" #dotnet20
|
||||
export WH_XDG_OPEN="rtf"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# info_ru: Программный комплекс СуперОкна для фабрик по изготовлению окон и дверей.
|
||||
########################################################################
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="swnd"
|
||||
export PROG_NAME="СуперОкна 8"
|
||||
export PROG_ICON="swnd"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# info_ru: Бесплатная версия программы СуперОкна для небольших предприятий.
|
||||
########################################################################
|
||||
export WH_WINE_USE="wine_x_tkg_10-0_amd64"
|
||||
export WH_WINE_USE="wine-10.12.1-alt1-i586"
|
||||
export WINEPREFIX="swnd"
|
||||
export PROG_NAME="СуперОкна 8 бесплатная"
|
||||
export PROG_ICON="swnd"
|
||||
|
||||
@@ -49,6 +49,8 @@ else
|
||||
done
|
||||
fi
|
||||
|
||||
export WH_DIST_DIR="$USER_WORK_PATH/dist"
|
||||
|
||||
##### MESSAGES FUNCTIONS #####
|
||||
if [[ $WH_USE_GUI != "1" ]] ; then
|
||||
print_error () { printf "\E[31m%s Ошибка: $* %s\e[0m\n" ;}
|
||||
@@ -65,7 +67,7 @@ fi
|
||||
print_var () { for vp in $@ ; do echo "${vp}=${!vp}" ; done ;}
|
||||
|
||||
fatal () {
|
||||
print_error "$@ Аварийное завершение работы WineHelper!"
|
||||
[[ -n "$@" ]] && print_error "$@"
|
||||
[[ -n "$WINESERVER" ]] && "$WINESERVER" -w
|
||||
exit 1
|
||||
}
|
||||
@@ -82,12 +84,13 @@ print_confirmation () {
|
||||
|
||||
##### CHECK NOEXEC FOR /HOME #####
|
||||
if mount -l | grep -E "[[:space:]]/home[[:space:]]" | grep -q "noexec" ; then
|
||||
# export WH_DIST_DIR="/opt/winehelper/dist"
|
||||
# print_warning "/home примонтирован в /etc/fstab с аргументом noexec.\nИспользуем каталог $WH_DIST_DIR"
|
||||
fatal "/home примонтирован в /etc/fstab с аргументом noexec.\nЗапуск портативной версии wine невозможен из домашнего каталога."
|
||||
export WH_NOEXEC_HOME="1"
|
||||
export WH_TMP_DIR="/var/tmp/winehelper-$USER/tmp"
|
||||
else
|
||||
export WH_DIST_DIR="$USER_WORK_PATH/dist"
|
||||
export WH_NOEXEC_HOME="0"
|
||||
export WH_TMP_DIR="$USER_WORK_PATH/tmp"
|
||||
fi
|
||||
[[ ! -d "$WH_TMP_DIR" ]] && mkdir -p "$WH_TMP_DIR"
|
||||
|
||||
if [[ $1 == "gui" ]] ; then
|
||||
export WH_USE_GUI="1"
|
||||
@@ -121,7 +124,6 @@ fi
|
||||
##### DEFAULT VARIABLES #####
|
||||
WH_VULKAN_LIBDIR="$USER_WORK_PATH/vulkan"
|
||||
WH_PREFIXES_DIR="$USER_WORK_PATH/prefixes"
|
||||
WH_TMP_DIR="$USER_WORK_PATH/tmp"
|
||||
|
||||
WH_IMAGE_PATH="$DATA_PATH/image"
|
||||
WH_DB_DIR="$DATA_PATH/database"
|
||||
@@ -717,7 +719,9 @@ EOF
|
||||
# Обновляем кэш desktop-файлов
|
||||
update-desktop-database "$HOME/.local/share/applications"
|
||||
|
||||
if [[ "$desktop_filename_arg" != "nocopy" ]] ; then
|
||||
if [[ "$desktop_filename_arg" != "nocopy" ]] \
|
||||
&& [[ "$WH_NOEXEC_HOME" != "1" ]]
|
||||
then
|
||||
desktop_path="$(xdg-user-dir DESKTOP)"
|
||||
print_info "В меню и на рабочем столе создан ярлык: $desktop_filename.desktop"
|
||||
cp -f "$USER_WORK_PATH/$desktop_filename.desktop" "$desktop_path"
|
||||
@@ -737,7 +741,7 @@ EOF
|
||||
-e "PROG_NAME=" -e "PROG_ICON=" -e "var_" -e "WH_MAIN_DECORATED" \
|
||||
-e "WH_VIRTUAL_DESKTOP" -e "WINE_TOP_WINDOW" -e "WH_FONT_SMOOTHING "\
|
||||
-e "WH_FONT_MSS_REPLACE" -e "FREETYPE_PROPERTIES" -e "WINE_D3D_CONFIG" \
|
||||
-e "WH_USE_MESA_GL_OVERRIDE" \
|
||||
-e "WH_USE_MESA_GL_OVERRIDE" -e "WINEDLLOVERRIDES" \
|
||||
"$INSTALL_SCRIPT" | awk '{$1=$1;print}' >> "$exe_file".whdb
|
||||
|
||||
print_info "Создан файл настроек для $exe_file"
|
||||
@@ -781,6 +785,8 @@ check_installed_programs () {
|
||||
elif [[ "$desktop_file" =~ ${2}.desktop ]] ; then
|
||||
export EXE_PATH
|
||||
return 0
|
||||
elif [[ -f "$2" ]] ; then
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
@@ -875,6 +881,105 @@ init_vkd3d () {
|
||||
done
|
||||
}
|
||||
|
||||
# Подготовка Wine для работы с noexec на /home
|
||||
# ELF бинарники копируются во временный каталог
|
||||
# Windows DLL остаются в /home через симлинки
|
||||
prepare_wine_noexec() {
|
||||
print_warning "/home примонтирован с noexec."
|
||||
|
||||
local wine_name="$1"
|
||||
local wine_home_dir="$WH_DIST_DIR/$wine_name"
|
||||
local dist_tmp_dir="/var/tmp/winehelper-$USER/dist"
|
||||
local wine_tmp_dir="$dist_tmp_dir/$wine_name"
|
||||
|
||||
export WH_DIST_DIR="$dist_tmp_dir"
|
||||
export WINEDIR="$wine_tmp_dir"
|
||||
|
||||
update_cpscp_proxy () {
|
||||
if [[ $UPDATE_CPCSP_PROXY == "1" ]] ; then
|
||||
print_info "Обновляем cpcsp_proxy во временном каталоге wine..."
|
||||
if [[ $WH_WINE_WOW64 == "1" ]] ; then
|
||||
cp -fv "$wine_home_dir/lib64/wine/x86_64-unix/cpcsp_proxy.so" "$wine_tmp_dir/lib64/wine/x86_64-unix/"
|
||||
cp -fv "$wine_home_dir/lib64/wine/x86_64-windows/cpcsp_proxy.dll" "$wine_tmp_dir/lib64/"
|
||||
cp -fv "$wine_home_dir/lib64/wine/i386-windows/cpcsp_proxy.dll" "$wine_tmp_dir/lib/"
|
||||
|
||||
local i386_proxy_dll="$wine_tmp_dir/lib64/wine/i386-windows/cpcsp_proxy.dll"
|
||||
if [[ ! -L "$i386_proxy_dll" ]] ; then
|
||||
try_remove_file "$i386_proxy_dll"
|
||||
try_force_link_file "$wine_tmp_dir/lib/cpcsp_proxy.dll" "$i386_proxy_dll"
|
||||
fi
|
||||
|
||||
local x86_64_proxy_dll="$wine_tmp_dir/lib64/wine/x86_64-windows/cpcsp_proxy.dll"
|
||||
if [[ ! -L "$x86_64_proxy_dll" ]] ; then
|
||||
try_remove_file "$x86_64_proxy_dll"
|
||||
try_force_link_file "$wine_tmp_dir/lib64/cpcsp_proxy.dll" "$x86_64_proxy_dll"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ -f "$wine_tmp_dir/bin/wine" ]] ; then
|
||||
update_cpscp_proxy
|
||||
print_ok "Wine готов для работы с noexec. Пропускаем настройку..."
|
||||
return 0
|
||||
fi
|
||||
print_info "Частичное копирование Wine во временный каталог..."
|
||||
|
||||
create_new_dir "$wine_tmp_dir/bin"
|
||||
cp -a "$wine_home_dir/bin/"* "$wine_tmp_dir/bin/"
|
||||
|
||||
create_new_dir "$wine_tmp_dir/lib/wine"
|
||||
if [[ -f "$wine_home_dir/lib/wine/wine" ]] \
|
||||
|| [[ -f "$wine_home_dir/lib/wine/wine64" ]]
|
||||
then
|
||||
cp -a "$wine_home_dir/lib/wine"/wine* "$wine_tmp_dir/lib/wine/"
|
||||
fi
|
||||
|
||||
for unix_dir in i386-unix x86_64-unix; do
|
||||
if [[ -d "$wine_home_dir/lib/wine/$unix_dir" ]] ; then
|
||||
cp -a "$wine_home_dir/lib/wine/$unix_dir" "$wine_tmp_dir/lib/wine/"
|
||||
fi
|
||||
done
|
||||
|
||||
for win_dir in i386-windows x86_64-windows; do
|
||||
if [[ -d "$wine_home_dir/lib/wine/$win_dir" ]] ; then
|
||||
try_force_link_dir "$wine_home_dir/lib/wine/$win_dir" "$wine_tmp_dir/lib/wine/$win_dir"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -d "$wine_home_dir/lib/gstreamer-1.0" ]] ; then
|
||||
cp -a "$wine_home_dir/lib/gstreamer-1.0" "$wine_tmp_dir/lib/"
|
||||
fi
|
||||
|
||||
if [[ -L "$wine_home_dir/lib64/wine" ]] ; then
|
||||
create_new_dir "$wine_tmp_dir/lib64/"
|
||||
try_force_link_dir "$wine_tmp_dir/lib/wine/" "$wine_tmp_dir/lib64/"
|
||||
elif [[ -d "$wine_home_dir/lib64" ]]; then
|
||||
create_new_dir "$wine_tmp_dir/lib64/wine"
|
||||
for unix_dir in i386-unix x86_64-unix; do
|
||||
if [[ -d "$wine_home_dir/lib64/wine/$unix_dir" ]] ; then
|
||||
cp -a "$wine_home_dir/lib64/wine/$unix_dir" "$wine_tmp_dir/lib64/wine/"
|
||||
fi
|
||||
done
|
||||
for win_dir in i386-windows x86_64-windows; do
|
||||
if [[ -d "$wine_home_dir/lib64/wine/$win_dir" ]] ; then
|
||||
try_force_link_dir "$wine_home_dir/lib64/wine/$win_dir" "$wine_tmp_dir/lib64/wine/$win_dir"
|
||||
fi
|
||||
done
|
||||
if [[ -d "$wine_home_dir/lib64/gstreamer-1.0" ]] ; then
|
||||
cp -a "$wine_home_dir/lib64/gstreamer-1.0" "$wine_tmp_dir/lib64/"
|
||||
fi
|
||||
fi
|
||||
|
||||
update_cpscp_proxy
|
||||
try_force_link_file "$wine_home_dir/cpcsp_proxy.ver" "$wine_tmp_dir/cpcsp_proxy.ver"
|
||||
|
||||
try_force_link_dir "$wine_home_dir/share" "$wine_tmp_dir/share"
|
||||
try_copy_file "$wine_home_dir/version" "$wine_tmp_dir/"
|
||||
|
||||
print_ok "Wine подготовлен для работы с noexec"
|
||||
}
|
||||
|
||||
init_wine_ver () {
|
||||
if [[ "$WH_WINE_USE" != system* ]] ; then
|
||||
export WINEDIR="$WH_DIST_DIR/$WH_WINE_USE"
|
||||
@@ -902,6 +1007,13 @@ init_wine_ver () {
|
||||
try_remove_dir "$WINEDIR/files"
|
||||
fi
|
||||
|
||||
if [[ ! -d "$WINEDIR/lib64/wine" ]] \
|
||||
&& [[ -d "$WINEDIR/lib/wine/x86_64-unix" ]]
|
||||
then
|
||||
create_new_dir "$WINEDIR/lib64/"
|
||||
try_force_link_dir "$WINEDIR/lib/wine/" "$WINEDIR/lib64/"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$WINEDIR/version" ]] ; then
|
||||
echo "$WH_WINE_USE" > "$WINEDIR/version"
|
||||
fi
|
||||
@@ -913,6 +1025,50 @@ init_wine_ver () {
|
||||
check_deps_i586
|
||||
fi
|
||||
|
||||
if [[ $WH_USE_CPCSP_PROXY == "1" ]] ; then
|
||||
unset UPDATE_CPCSP_PROXY
|
||||
local cpcsp_proxy_ver
|
||||
if [[ $WH_WINE_WOW64 == "1" ]]
|
||||
then cpcsp_proxy_ver="$CPCSP_PROXY_WOW64_VER"
|
||||
else cpcsp_proxy_ver="$CPCSP_PROXY_X86_64_VER"
|
||||
fi
|
||||
if [[ ! -f "$WINEDIR/cpcsp_proxy.ver" ]] \
|
||||
|| ! grep -q "$cpcsp_proxy_ver" "$WINEDIR/cpcsp_proxy.ver"
|
||||
then
|
||||
export UPDATE_CPCSP_PROXY="1"
|
||||
local cpcsp_proxy_name="wine-cpcsp_proxy-$cpcsp_proxy_ver"
|
||||
local cpcsp_proxy_url="$CLOUD_URL/$cpcsp_proxy_name.tar.xz"
|
||||
|
||||
try_download cloud "$cpcsp_proxy_url" "$WH_TMP_DIR/$cpcsp_proxy_name.tar.xz" check256sum
|
||||
unpack "$WH_TMP_DIR/$cpcsp_proxy_name.tar.xz" "$WH_TMP_DIR"
|
||||
|
||||
if [[ $WH_WINE_WOW64 == "1" ]] ; then
|
||||
for dir_name in x86_64-unix i386-windows x86_64-windows ; do
|
||||
if [[ -f "$WINEDIR/lib64/wine/$dir_name/cpcsp_proxy.dll" ]] ; then
|
||||
try_remove_file "$WINEDIR/lib64/wine/$dir_name/cpcsp_proxy.dll"
|
||||
fi
|
||||
cp -f "$WH_TMP_DIR/$cpcsp_proxy_name/$dir_name"/* "$WINEDIR/lib64/wine/$dir_name/"
|
||||
done
|
||||
else
|
||||
for dir_name in i386-unix i386-windows ; do
|
||||
cp -f "$WH_TMP_DIR/$cpcsp_proxy_name/$dir_name"/* "$WINEDIR/lib/wine/$dir_name/"
|
||||
done
|
||||
if [[ -d "$WINEDIR/lib64" ]] ; then
|
||||
for dir_name in x86_64-unix x86_64-windows ; do
|
||||
cp -f "$WH_TMP_DIR/$cpcsp_proxy_name/$dir_name"/* "$WINEDIR/lib64/wine/$dir_name/"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
try_remove_dir "$WH_TMP_DIR/$cpcsp_proxy_name"
|
||||
echo "$cpcsp_proxy_ver" > "$WINEDIR/cpcsp_proxy.ver"
|
||||
fi
|
||||
fi
|
||||
|
||||
# При noexec на /home: готовим временную копию ELF файлов wine перед запуском
|
||||
if [[ "$WH_NOEXEC_HOME" == "1" ]] ; then
|
||||
prepare_wine_noexec "$WH_WINE_USE"
|
||||
fi
|
||||
|
||||
export WINE="$WINEDIR/bin/wine"
|
||||
export WINELOADER="$WINEDIR/bin/wine"
|
||||
export WINESERVER="$WINEDIR/bin/wineserver"
|
||||
@@ -925,13 +1081,6 @@ init_wine_ver () {
|
||||
var_ld_library_path_update "$WINEDIR/lib"
|
||||
export WINEDLLPATH="$WINEDIR/lib/wine"
|
||||
|
||||
if [[ ! -d "$WINEDIR/lib64/wine" ]] \
|
||||
&& [[ -d "$WINEDIR/lib/wine/x86_64-unix" ]]
|
||||
then
|
||||
create_new_dir "$WINEDIR/lib64/"
|
||||
try_force_link_dir "$WINEDIR/lib/wine/" "$WINEDIR/lib64/"
|
||||
fi
|
||||
|
||||
if [[ -d "$WINEDIR/lib64" ]] \
|
||||
&& [[ ! -L "$WINEDIR/lib64/wine" ]]
|
||||
then
|
||||
@@ -945,34 +1094,6 @@ init_wine_ver () {
|
||||
export GST_PLUGIN_SYSTEM_PATH_1_0+=":$WINEDIR/lib64/gstreamer-1.0"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $WH_USE_CPCSP_PROXY == "1" ]] ; then
|
||||
local cpcsp_proxy_ver
|
||||
if [[ $WH_WINE_WOW64 == "1" ]]
|
||||
then cpcsp_proxy_ver="$CPCSP_PROXY_WOW64_VER"
|
||||
else cpcsp_proxy_ver="$CPCSP_PROXY_X86_64_VER"
|
||||
fi
|
||||
if [[ ! -f "$WINEDIR/cpcsp_proxy.ver" ]] \
|
||||
|| ! grep -q "$cpcsp_proxy_ver" "$WINEDIR/cpcsp_proxy.ver"
|
||||
then
|
||||
CPCSP_PROXY_NAME="wine-cpcsp_proxy-$cpcsp_proxy_ver"
|
||||
CPCSP_PROXY_URL="$CLOUD_URL/$CPCSP_PROXY_NAME.tar.xz"
|
||||
|
||||
try_download cloud "$CPCSP_PROXY_URL" "$WH_TMP_DIR/$CPCSP_PROXY_NAME.tar.xz" check256sum
|
||||
unpack "$WH_TMP_DIR/$CPCSP_PROXY_NAME.tar.xz" "$WH_TMP_DIR"
|
||||
|
||||
if [[ $WH_WINE_WOW64 == "1" ]] ; then
|
||||
cp -fr "$WH_TMP_DIR/$CPCSP_PROXY_NAME/"* "$WINEDIR/lib64/wine/"
|
||||
else
|
||||
cp -fr "$WH_TMP_DIR/$CPCSP_PROXY_NAME/"i386-* "$WINEDIR/lib/wine/"
|
||||
if [[ -d "$WINEDIR/lib64" ]] ; then
|
||||
cp -fr "$WH_TMP_DIR/$CPCSP_PROXY_NAME/"x86_64-* "$WINEDIR/lib64/wine/"
|
||||
fi
|
||||
fi
|
||||
try_remove_dir "$WH_TMP_DIR/$CPCSP_PROXY_NAME"
|
||||
echo "$cpcsp_proxy_ver" > "$WINEDIR/cpcsp_proxy.ver"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# use system WINE
|
||||
if [[ ! -f "/usr/bin/wine" ]] ; then
|
||||
@@ -1220,16 +1341,18 @@ init_wineprefix () {
|
||||
wait_wineserver
|
||||
fi
|
||||
|
||||
local winver
|
||||
if [[ "${WH_WINDOWS_VER,,}" == "xp64" ]]
|
||||
then winver="xp 64"
|
||||
else winver="$WH_WINDOWS_VER"
|
||||
fi
|
||||
if [[ -f "$WINEPREFIX/system.reg" ]] \
|
||||
&& ! grep -iq "Microsoft Windows $WH_WINDOWS_VER" "$WINEPREFIX/system.reg"
|
||||
&& ! grep -iq "Microsoft Windows ${winver}\"" "$WINEPREFIX/system.reg"
|
||||
then
|
||||
if [[ $(echo "$WH_WINDOWS_VER" | sed 's/.*/\L&/') == "xp" ]] \
|
||||
&& [[ "$WINEARCH" != "win32" ]]
|
||||
then export WH_WINDOWS_VER="xp64"
|
||||
fi
|
||||
"$WINELOADER" winecfg -v $(echo "win${WH_WINDOWS_VER}" | sed 's/.*/\L&/')
|
||||
[[ "$winver" == "xp 64" ]] && winver="xp64"
|
||||
"$WINELOADER" winecfg -v $(echo "win${winver}" | sed 's/.*/\L&/')
|
||||
wait_wineserver
|
||||
print_info "Windows версия изменена на win${WH_WINDOWS_VER}"
|
||||
print_info "Windows версия изменена на win${winver}"
|
||||
fi
|
||||
|
||||
if [[ $WH_USE_MESA_GL_OVERRIDE == "1" ]] \
|
||||
@@ -1307,11 +1430,13 @@ init_wineprefix () {
|
||||
|
||||
# добавление ассоциаций файлов для запуска нативного приложения из wine
|
||||
# пример переменной: WH_XDG_OPEN="txt doc pdf"
|
||||
check_variables WH_XDG_OPEN "0"
|
||||
check_variables WH_XDG_OPEN "default"
|
||||
|
||||
local WRAPPER="${WH_TMP_DIR}/wh-xdg-open.sh"
|
||||
local XDG_OPEN_REG="Software\Classes\xdg-open\shell\open\command"
|
||||
if [[ $WH_XDG_OPEN != "0" ]] ; then
|
||||
if [[ $WH_XDG_OPEN == "default" ]] ; then
|
||||
: # пропускаем изменения в реестре
|
||||
elif [[ $WH_XDG_OPEN != "0" ]] ; then
|
||||
# проверяем на наличие запрещённых расширений
|
||||
local forbidden_extensions="cpl dll exe lnk msi"
|
||||
for ext in $WH_XDG_OPEN; do
|
||||
@@ -1351,16 +1476,18 @@ init_wineprefix () {
|
||||
|
||||
# настраиваем префикс для работы с cpcsp_proxy
|
||||
if [[ $WH_USE_CPCSP_PROXY == "1" ]] ; then
|
||||
if ! grep -q "cpcsp_proxy.dll" "$WINEPREFIX/system.reg" \
|
||||
|| [[ $REBOOT_PFX == "1" ]]
|
||||
if [[ $UPDATE_CPCSP_PROXY == "1" ]] \
|
||||
|| [[ $REBOOT_PFX == "1" ]] \
|
||||
|| ! grep -q "cpcsp_proxy.dll" "$WINEPREFIX/system.reg"
|
||||
then
|
||||
print_info "Запускаем настройку cpcsp_proxy..."
|
||||
CPCSP_PROXY_OK="0"
|
||||
|
||||
try_copy_wine_dll_to_pfx_64 "cpcsp_proxy.dll"
|
||||
try_copy_wine_dll_to_pfx_32 "cpcsp_proxy.dll"
|
||||
|
||||
if [[ $WH_WINE_WOW64 == "1" ]] ; then
|
||||
print_info "Запускаем настройку cpcsp_proxy версии $CPCSP_PROXY_WOW64_VER"
|
||||
|
||||
print_info "Запускаем регистрацию 64-битного cpcsp_proxy.dll..."
|
||||
env WINEDEBUG="fixme-all" "$WINE" "C:\\windows\\system32\\regsvr32.exe" \
|
||||
/s /n /i cpcsp_proxy.dll 2>&1 | tee "$WINEPREFIX/cpcsp_setup.log"
|
||||
@@ -1371,6 +1498,8 @@ init_wineprefix () {
|
||||
/s /n /i cpcsp_proxy.dll 2>&1 | tee -a "$WINEPREFIX/cpcsp_setup.log"
|
||||
wait_wineserver
|
||||
else
|
||||
print_info "Запускаем настройку cpcsp_proxy версии $CPCSP_PROXY_X86_64_VER"
|
||||
|
||||
try_copy_wine_dll_to_pfx_64 "cpcsp_proxy_setup.exe"
|
||||
try_copy_wine_dll_to_pfx_32 "cpcsp_proxy_setup.exe"
|
||||
|
||||
@@ -1388,8 +1517,14 @@ init_wineprefix () {
|
||||
done
|
||||
fi
|
||||
|
||||
if grep -q "failed to load /opt/cprocsp/" "$WINEPREFIX/cpcsp_setup.log" ; then
|
||||
print_error "Проверьте правильность установки КриптоПро в системе.\n Инструкция: https://www.altlinux.org/CryptoPro"
|
||||
if [[ ! -d "/opt/cprocsp/" ]] \
|
||||
|| grep -q "failed to load /opt/cprocsp/" "$WINEPREFIX/cpcsp_setup.log"
|
||||
then
|
||||
print_error "Проверьте правильность установки КриптоПро в системе!
|
||||
|
||||
Официальный сайт КриптоПро: https://cryptopro.ru/products/csp
|
||||
Инструкция установки КриптоПро в систему: https://www.altlinux.org/CryptoPro
|
||||
"
|
||||
else
|
||||
CPCSP_PROXY_OK="1"
|
||||
fi
|
||||
@@ -1397,7 +1532,7 @@ init_wineprefix () {
|
||||
|
||||
if [[ $CPCSP_PROXY_OK == "1" ]]
|
||||
then print_info "Настройка cpcsp_proxy успешно завершена."
|
||||
else fatal "Ошибка во время настройки cpcsp_proxy."
|
||||
else fatal
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
@@ -1423,8 +1558,8 @@ init_wineprefix () {
|
||||
fi
|
||||
for var in WH_WINE_USE BASE_PFX WINEARCH WH_WINDOWS_VER WINEESYNC WINEFSYNC \
|
||||
STAGING_SHARED_MEMORY WINE_LARGE_ADDRESS_AWARE WH_USE_SHADER_CACHE WH_USE_WINE_DXGI \
|
||||
WINE_CPU_TOPOLOGY DXVK_VER VKD3D_VER WH_XDG_OPEN WH_USE_CPCSP_PROXY WH_USE_EXTRA_FONTS \
|
||||
WINEUSERNAME
|
||||
WINE_CPU_TOPOLOGY DXVK_VER VKD3D_VER WH_XDG_OPEN WH_USE_EXTRA_FONTS \
|
||||
WINEUSERNAME WH_USE_CPCSP_PROXY CPCSP_PROXY_X86_64_VER CPCSP_PROXY_WOW64_VER
|
||||
do
|
||||
update_last_conf_var "$var" "${!var}"
|
||||
done
|
||||
@@ -1604,6 +1739,32 @@ wine_run () {
|
||||
|
||||
echo -e "\n##### Лог WINE #####" | tee -a "$log_file"
|
||||
$MANGOHUD_RUN "$WINELOADER" $wh_add_args "$win_file_exec" "$@" 2>&1 | tee -a "$log_file"
|
||||
|
||||
# Проверка размера лога и создание архива при превышении порога
|
||||
local MAX_LOG_SIZE_MB=10
|
||||
local MAX_LOG_SIZE_BYTES=$((MAX_LOG_SIZE_MB * 1048576))
|
||||
local log_size_bytes log_size_mb
|
||||
log_size_bytes=$(stat -c%s "$log_file" 2>/dev/null || echo 0)
|
||||
log_size_mb=$((log_size_bytes / 1048576))
|
||||
|
||||
if (( log_size_bytes > MAX_LOG_SIZE_BYTES )); then
|
||||
local timestamp archive_name archive_path
|
||||
timestamp=$(date +"%d.%m.%Y-%H.%M")
|
||||
archive_name="log_${PREFIX_NAME}_${win_file_name%.*}_${timestamp}.tar.xz"
|
||||
archive_path="$log_dir/$archive_name"
|
||||
|
||||
print_warning "Файл лога больше ${MAX_LOG_SIZE_MB}Мб (размер: ${log_size_mb}Мб)."
|
||||
print_warning "Создаем сжатый архив: $archive_name"
|
||||
|
||||
tar -cJf "$archive_path" -C "$log_dir" "$(basename "$log_file")"
|
||||
|
||||
if [[ -f "$archive_path" ]]; then
|
||||
print_ok "Архив успешно создан: $archive_path"
|
||||
rm -f "$log_file"
|
||||
else
|
||||
print_error "Не удалось создать архив: $archive_path"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
$MANGOHUD_RUN "$WINELOADER" $wh_add_args "$win_file_exec" "$@"
|
||||
fi
|
||||
@@ -1612,9 +1773,47 @@ wine_run () {
|
||||
cd "$DRIVE_C"
|
||||
}
|
||||
|
||||
wine_debug_run () {
|
||||
local winedbg_args win_file_exec win_file_path win_file_name wh_add_args
|
||||
|
||||
winedbg_args="$1"
|
||||
[[ "$winedbg_args" == "--interactive" ]] && winedbg_args=""
|
||||
shift
|
||||
|
||||
if [[ -f "$1" ]]; then
|
||||
win_file_exec="$(readlink -f "$1")"
|
||||
win_file_path="$(dirname "$win_file_exec")"
|
||||
win_file_name="$(basename "$win_file_exec")"
|
||||
else
|
||||
fatal "Файл не найден: $1"
|
||||
fi
|
||||
|
||||
shift
|
||||
cd "$win_file_path"
|
||||
|
||||
local log_dir log_file
|
||||
log_dir="$HOME/winehelper_backup_log"
|
||||
log_file="$log_dir/winedbg_${PREFIX_NAME}_${win_file_name%.*}.log"
|
||||
create_new_dir "$log_dir"
|
||||
date > "$log_file"
|
||||
echo -e "\n##### Основные переменные #####" | tee -a "$log_file"
|
||||
env | grep -e "WH_" -e "WINE" -e "DXVK" -e "VKD3D" -e "LD_" \
|
||||
| grep -v "ICON" | sort | tee -a "$log_file"
|
||||
echo -e "\n##### WineHelper Debug Mode: winedbg $winedbg_args #####" | tee -a "$log_file"
|
||||
|
||||
print_info "Запуск отладчика (режим: $winedbg_args)"
|
||||
print_warning "Лог отладки будет сохранен по пути: $log_file"
|
||||
|
||||
"$WINELOADER" winedbg $winedbg_args "$win_file_exec" "$@" 2>&1 | tee -a "$log_file"
|
||||
|
||||
wait_wineserver
|
||||
cd "$DRIVE_C"
|
||||
}
|
||||
|
||||
wine_run_install () {
|
||||
print_info "Запускаем установку: $1."
|
||||
case "$WH_INSTALL_MODE" in
|
||||
"another") print_warning "Используется скрипт сторонний установки!" ;;
|
||||
"manual") print_warning "Рекомендуется не менять пути для установки приложения!" ;;
|
||||
"test") print_warning "Установка приложения из списка экспериментальных скриптов." ;;
|
||||
esac
|
||||
@@ -1643,6 +1842,9 @@ run_autoinstall () {
|
||||
elif [[ -f "$WH_TESTINSTALL_DIR/$INSTALL_SCRIPT_NAME" ]] ; then
|
||||
INSTALL_SCRIPT="$WH_TESTINSTALL_DIR/$INSTALL_SCRIPT_NAME"
|
||||
WH_INSTALL_MODE="test"
|
||||
elif [[ -f "$1" ]] ; then
|
||||
INSTALL_SCRIPT="$1"
|
||||
WH_INSTALL_MODE="another"
|
||||
else
|
||||
INSTALL_SCRIPT="0"
|
||||
fi
|
||||
@@ -2074,6 +2276,7 @@ remove_winehelper () {
|
||||
# Удаление рабочих каталогов
|
||||
try_remove_dir "$USER_WORK_PATH"
|
||||
try_remove_dir "$HOME/winehelper_backup_log"
|
||||
try_remove_dir "/var/tmp/winehelper-$USER"
|
||||
|
||||
# Удаление файлов меню
|
||||
try_remove_dir "$WH_MENU_DIR"
|
||||
@@ -2082,7 +2285,7 @@ remove_winehelper () {
|
||||
|
||||
# Удаление desktop-файлов
|
||||
for desktop_file in "$(xdg-user-dir DESKTOP)"/*.desktop; do
|
||||
if grep -q "Exec=env \"$RUN_SCRIPT\"" "$desktop_file"; then
|
||||
if [[ -f "$desktop_file" ]] && grep -q "Exec=env \"$RUN_SCRIPT\"" "$desktop_file"; then
|
||||
try_remove_file "$desktop_file"
|
||||
fi
|
||||
done
|
||||
@@ -2170,9 +2373,9 @@ backup_prefix() {
|
||||
local backup_base_dir backup_archive_name backup_dest_path temp_backup_dir temp_prefix_dir temp_users_dir
|
||||
backup_base_dir="$HOME/winehelper_backup_log"
|
||||
create_new_dir "$backup_base_dir"
|
||||
backup_archive_name="backup_${PREFIX_NAME}_$(date +%d.%m.%Y-%H.%M.%S).whpack"
|
||||
backup_archive_name="backup_${PREFIX_NAME}_$(date +%d.%m.%Y-%H.%M).whpack"
|
||||
backup_dest_path="$backup_base_dir/$backup_archive_name"
|
||||
temp_backup_dir="$WH_TMP_DIR/backup_${PREFIX_NAME}_$(date +%d.%m.%Y-%H.%M.%S)"
|
||||
temp_backup_dir="$WH_TMP_DIR/backup_${PREFIX_NAME}_$(date +%d.%m.%Y-%H.%M)"
|
||||
temp_prefix_dir="$temp_backup_dir/prefixes/$PREFIX_NAME"
|
||||
temp_users_dir="$temp_prefix_dir/drive_c/users"
|
||||
|
||||
@@ -2205,13 +2408,11 @@ backup_prefix() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -n "$WH_WINE_USE" ]] \
|
||||
&& [[ $WH_WINE_USE != system* ]] \
|
||||
&& [[ -d "$WH_DIST_DIR/$WH_WINE_USE" ]]
|
||||
then
|
||||
if [[ $WH_WINE_USE != system* ]] ; then
|
||||
init_wine_ver
|
||||
print_info "Копирование используемой версии WINE: $WH_WINE_USE"
|
||||
|
||||
if cp -a "$WH_DIST_DIR/$WH_WINE_USE" "$temp_backup_dir/dist/"
|
||||
if cp -aL "$WH_DIST_DIR/$WH_WINE_USE" "$temp_backup_dir/dist/"
|
||||
then print_ok "WINE скопирован."
|
||||
else fatal "Не удалось скопировать WINE."
|
||||
fi
|
||||
@@ -2221,7 +2422,7 @@ backup_prefix() {
|
||||
echo "$PREFIX_NAME" > "$temp_backup_dir/prefix_name.whmeta"
|
||||
|
||||
print_info "Создание squashfs архива..."
|
||||
if mksquashfs "$temp_backup_dir" "$backup_dest_path" -comp zstd ; then
|
||||
if mksquashfs "$temp_backup_dir" "$backup_dest_path" -comp xz ; then
|
||||
print_ok "Архив префикса $PREFIX_NAME успешно создан по пути:\n$backup_dest_path"
|
||||
else
|
||||
print_error "Не удалось создать архив префикса $PREFIX_NAME."
|
||||
@@ -2285,7 +2486,7 @@ restore_prefix() {
|
||||
|
||||
if [[ -d "$WH_PREFIXES_DIR/$prefix_name" ]] ; then
|
||||
try_remove_dir "$temp_extract_dir"
|
||||
fatal "Префикс $prefix_name уже существует!\n Удалите его, если действительно желаете восстановить префикс заново.\n Команда для удаления: $SCRIPT_NAME remove-prefix $prefix_name"
|
||||
fatal "Префикс $prefix_name уже существует!\n Удалите его перед восстановлением префикса заново.\n Команда для удаления: $SCRIPT_NAME remove-prefix $prefix_name"
|
||||
fi
|
||||
|
||||
if [[ -d "$temp_extract_dir/prefixes/$prefix_name" ]] ; then
|
||||
@@ -2306,16 +2507,17 @@ restore_prefix() {
|
||||
try_remove_dir "$temp_extract_dir"
|
||||
fatal "Не удалось восстановить префикс $prefix_name."
|
||||
else
|
||||
print_ok "Префикс $prefix_name восстановлен."
|
||||
|
||||
export WINEPREFIX="$WH_PREFIXES_DIR/$prefix_name"
|
||||
if [[ -f "$WINEPREFIX/last.conf" ]] ; then
|
||||
source "$WINEPREFIX/last.conf"
|
||||
export REBOOT_PFX="1"
|
||||
prepair_wine
|
||||
if [[ -f "$WINEPREFIX/restore_pfx.sh" ]] ; then
|
||||
source "$WINEPREFIX/restore_pfx.sh"
|
||||
try_copy_file "$WINEPREFIX/restore_pfx.sh" "$WH_TMP_DIR/restore_${prefix_name}_pfx.sh"
|
||||
source "$WH_TMP_DIR/restore_${prefix_name}_pfx.sh"
|
||||
fi
|
||||
fi
|
||||
print_ok "Префикс $prefix_name восстановлен."
|
||||
|
||||
if [[ -f "$WH_PREFIXES_DIR/$prefix_name/desktop.list" ]] ; then
|
||||
print_info "Восстановление ярлыков для префикса $prefix_name..."
|
||||
@@ -2342,6 +2544,7 @@ restore_prefix() {
|
||||
fi
|
||||
|
||||
try_remove_dir "$temp_extract_dir"
|
||||
try_remove_file "$WH_TMP_DIR/restore_${prefix_name}_pfx.sh"
|
||||
print_ok "Полное восстановление префикса $prefix_name завершено."
|
||||
return 0
|
||||
}
|
||||
@@ -2581,7 +2784,12 @@ wh_info () {
|
||||
change-wine [версия] изменить версию Wine для текущего префикса
|
||||
|
||||
installed список установленных программ
|
||||
run [программа] запуск программы (отладка)
|
||||
run [программа] запуск программы
|
||||
debug [флаги] [программа] запуск программы в отладчике winedbg
|
||||
Флаги:
|
||||
--auto, -a автоматический backtrace
|
||||
--gdb, -g режим gdb
|
||||
--interactive, -i интерактивный режим
|
||||
remove-all удалить WineHelper и все связанные данные
|
||||
create-prefix создать префикс
|
||||
remove-prefix [имя_префикса] удалить префикс и все связанные данные
|
||||
@@ -2651,6 +2859,22 @@ case "$arg1" in
|
||||
wine_run "$WIN_FILE_EXEC" "$@"
|
||||
fi
|
||||
;;
|
||||
debug)
|
||||
case "$1" in
|
||||
--gdb|-g) debug_flag="--gdb" ;;
|
||||
--interactive|-i) debug_flag="--interactive" ;;
|
||||
--auto|-a) debug_flag="--auto" ;;
|
||||
*) fatal "Неизвестный флаг отладки!" ;;
|
||||
esac
|
||||
shift
|
||||
|
||||
if check_installed_programs check_only "$1" ; then
|
||||
WIN_FILE_EXEC="$(readlink -f "$EXE_PATH")"
|
||||
shift
|
||||
prepair_wine
|
||||
wine_debug_run $debug_flag "$WIN_FILE_EXEC" "$@"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
if [[ -f "$arg1" ]] ; then
|
||||
WIN_FILE_EXEC="$(readlink -f "$arg1")"
|
||||
@@ -2665,6 +2889,9 @@ case "$arg1" in
|
||||
else
|
||||
wine_run "$WIN_FILE_EXEC" "$@"
|
||||
fi
|
||||
elif [[ "${arg1,,}" =~ \.dll ]] ; then
|
||||
prepair_wine
|
||||
wine_run "$arg1"
|
||||
else
|
||||
print_error "Команды $arg1 не существует."
|
||||
wh_info
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
GUI пакет для winehelper.
|
||||
|
||||
Модульная структура:
|
||||
- dialogs: Диалоги (выбор версий, настройки и т.д.)
|
||||
- widgets: Кастомные виджеты
|
||||
- services: Бизнес-логика (кэширование, установка и т.д.)
|
||||
- parsers: Парсеры (sha256sum.list, winetricks output)
|
||||
- utils: Утилиты (обработчики логов и т.д.)
|
||||
"""
|
||||
|
||||
# Parsers
|
||||
from winehelperqt.parsers.sha256_parser import Sha256Parser
|
||||
from winehelperqt.parsers.winetricks_parser import WinetricksOutputParser
|
||||
from winehelperqt.parsers.script_parser import ScriptParser
|
||||
|
||||
# Utils
|
||||
from winehelperqt.utils.log_handler import LogProgressHandler, MultiLogHandler
|
||||
from winehelperqt.utils.cache_service import CacheService, FileCacheService
|
||||
|
||||
# Services
|
||||
from winehelperqt.services.winetricks_service import WinetricksCacheService
|
||||
from winehelperqt.services.winetricks_service import WinetricksLogReader
|
||||
from winehelperqt.services.prefix_manager import PrefixManager
|
||||
from winehelperqt.services.process_handler import ProcessHandler
|
||||
from winehelperqt.services.process_handler import CommandRunner
|
||||
from winehelperqt.services.desktop_file_manager import DesktopFileManager
|
||||
from winehelperqt.services.application_installer import ApplicationInstaller
|
||||
from winehelperqt.services.script_manager import ScriptManager
|
||||
from winehelperqt.services.script_manager import ScriptInfo
|
||||
from winehelperqt.services.installation_manager import InstallationManager
|
||||
from winehelperqt.services.app_launcher import AppLauncher
|
||||
from winehelperqt.services.settings_manager import SettingsManager
|
||||
|
||||
# Widgets
|
||||
from winehelperqt.widgets.icon_manager import IconManager
|
||||
|
||||
# Dialogs
|
||||
from winehelperqt.dialogs.winetricks_manager import WinetricksManagerDialog
|
||||
|
||||
__all__ = [
|
||||
# Parsers
|
||||
'Sha256Parser',
|
||||
'WinetricksOutputParser',
|
||||
'ScriptParser',
|
||||
|
||||
# Utils
|
||||
'LogProgressHandler',
|
||||
'MultiLogHandler',
|
||||
'CacheService',
|
||||
'FileCacheService',
|
||||
|
||||
# Services
|
||||
'WinetricksCacheService',
|
||||
'WinetricksLogReader',
|
||||
'PrefixManager',
|
||||
'ProcessHandler',
|
||||
'CommandRunner',
|
||||
'DesktopFileManager',
|
||||
'ApplicationInstaller',
|
||||
'ScriptManager',
|
||||
'ScriptInfo',
|
||||
'InstallationManager',
|
||||
'AppLauncher',
|
||||
'SettingsManager',
|
||||
|
||||
# Widgets
|
||||
'IconManager',
|
||||
|
||||
# Dialogs
|
||||
'WinetricksManagerDialog',
|
||||
]
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Диалоги для winehelper GUI.
|
||||
"""
|
||||
from winehelperqt.dialogs.base_version_dialog import (
|
||||
BaseVersionSelectionDialog,
|
||||
WineVersionSelectionDialogBase,
|
||||
ComponentVersionSelectionDialogBase,
|
||||
)
|
||||
from winehelperqt.dialogs.winetricks_manager import WinetricksManagerDialog
|
||||
from winehelperqt.dialogs.wine_version import WineVersionSelectionDialog
|
||||
from winehelperqt.dialogs.component_version import ComponentVersionSelectionDialog
|
||||
from winehelperqt.dialogs.create_prefix import CreatePrefixDialog
|
||||
from winehelperqt.dialogs.file_associations import FileAssociationsDialog
|
||||
from winehelperqt.dialogs.settings import AdditionalSettingsDialog, AppAdditionalSettingsDialog
|
||||
|
||||
__all__ = [
|
||||
# Base classes
|
||||
'BaseVersionSelectionDialog',
|
||||
'WineVersionSelectionDialogBase',
|
||||
'ComponentVersionSelectionDialogBase',
|
||||
|
||||
# Dialogs
|
||||
'WinetricksManagerDialog',
|
||||
'WineVersionSelectionDialog',
|
||||
'ComponentVersionSelectionDialog',
|
||||
'CreatePrefixDialog',
|
||||
'FileAssociationsDialog',
|
||||
'AdditionalSettingsDialog',
|
||||
'AppAdditionalSettingsDialog',
|
||||
]
|
||||
@@ -0,0 +1,321 @@
|
||||
"""
|
||||
Базовые классы для диалогов выбора версий.
|
||||
Устраняет дублирование кода между WineVersionSelectionDialog и ComponentVersionSelectionDialog.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, QScrollArea,
|
||||
QTabWidget, QLineEdit, QPushButton, QLabel, QWidget, QMessageBox
|
||||
)
|
||||
from PyQt5.QtCore import Qt, pyqtSignal
|
||||
|
||||
from winehelperqt.parsers.sha256_parser import Sha256Parser
|
||||
from winehelperqt.utils.environment import EnvironmentConfig
|
||||
|
||||
|
||||
class BaseVersionSelectionDialog(QDialog):
|
||||
"""
|
||||
Базовый класс для диалогов выбора версий.
|
||||
|
||||
Предоставляет общую функциональность:
|
||||
- Поле поиска с фильтрацией
|
||||
- Сетка кнопок с версиями
|
||||
- Базовая логика выбора версии
|
||||
"""
|
||||
|
||||
NUM_COLUMNS = 3
|
||||
|
||||
version_selected = pyqtSignal(str)
|
||||
|
||||
def __init__(self, title, parent=None, min_width=600, min_height=400):
|
||||
"""
|
||||
Инициализирует базовый диалог выбора версий.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
|
||||
self.setWindowTitle(title)
|
||||
self.setMinimumSize(min_width, min_height)
|
||||
self.setModal(True)
|
||||
|
||||
self.selected_version = None
|
||||
self.buttons = []
|
||||
self.search_edit = None
|
||||
self.main_layout = None
|
||||
|
||||
self._setup_ui()
|
||||
|
||||
def _get_sha256_path(self):
|
||||
"""Возвращает путь к файлу sha256sum.list."""
|
||||
data_path = EnvironmentConfig.DATA_PATH
|
||||
if data_path:
|
||||
return os.path.join(data_path, "sha256sum.list")
|
||||
# Фолбэк: ищем рядом с этим файлом (для отладки)
|
||||
return os.path.join(os.path.dirname(__file__), '../../sha256sum.list')
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Создает базовый UI диалога."""
|
||||
self.main_layout = QVBoxLayout(self)
|
||||
|
||||
self.search_edit = QLineEdit()
|
||||
self.search_edit.setPlaceholderText("Поиск версии...")
|
||||
self.search_edit.textChanged.connect(self.filter_versions)
|
||||
self.main_layout.addWidget(self.search_edit)
|
||||
|
||||
self._create_version_container()
|
||||
|
||||
def _create_version_container(self):
|
||||
"""Создает контейнер для отображения версий."""
|
||||
raise NotImplementedError("Подкласс должен реализовать _create_version_container")
|
||||
|
||||
def load_versions(self):
|
||||
"""Загружает список версий."""
|
||||
raise NotImplementedError("Подкласс должен реализовать load_versions")
|
||||
|
||||
def filter_versions(self):
|
||||
"""Фильтрует версии по тексту поиска."""
|
||||
search_text = self.search_edit.text().lower()
|
||||
|
||||
visible_buttons = []
|
||||
for btn_widget in self.buttons:
|
||||
if search_text in btn_widget.text().lower():
|
||||
visible_buttons.append(btn_widget)
|
||||
|
||||
for btn_widget in self.buttons:
|
||||
btn_widget.setVisible(False)
|
||||
|
||||
for i, btn_widget in enumerate(visible_buttons):
|
||||
row, col = divmod(i, self.NUM_COLUMNS)
|
||||
if hasattr(self, 'grid_layout'):
|
||||
self.grid_layout.addWidget(btn_widget, row, col)
|
||||
btn_widget.setVisible(True)
|
||||
|
||||
def on_version_selected(self, version_name):
|
||||
"""Обрабатывает выбор версии."""
|
||||
self.selected_version = version_name
|
||||
self.version_selected.emit(version_name)
|
||||
self.accept()
|
||||
|
||||
def get_selected_version(self):
|
||||
"""Возвращает выбранную версию."""
|
||||
return self.selected_version
|
||||
|
||||
@staticmethod
|
||||
def create_grid_buttons(
|
||||
grid_layout,
|
||||
versions_list,
|
||||
on_click_callback,
|
||||
start_row=0,
|
||||
num_columns=3,
|
||||
is_download_tab=False,
|
||||
version_filter=None
|
||||
):
|
||||
"""Создает сетку кнопок для версий."""
|
||||
buttons = []
|
||||
row, col = start_row, 0
|
||||
|
||||
for version_data in versions_list:
|
||||
if isinstance(version_data, tuple):
|
||||
display_name, value_name = version_data
|
||||
else:
|
||||
display_name = value_name = version_data
|
||||
|
||||
btn = QPushButton(display_name)
|
||||
btn.clicked.connect(lambda checked, vn=value_name: on_click_callback(vn))
|
||||
|
||||
if version_filter and not is_download_tab:
|
||||
should_disable = version_filter(value_name)
|
||||
if should_disable:
|
||||
btn.setEnabled(False)
|
||||
btn.setToolTip("Эта версия несовместима с выбранной архитектурой.")
|
||||
|
||||
grid_layout.addWidget(btn, row, col)
|
||||
buttons.append(btn)
|
||||
|
||||
col += 1
|
||||
if col >= num_columns:
|
||||
col = 0
|
||||
row += 1
|
||||
|
||||
return buttons
|
||||
|
||||
|
||||
class WineVersionSelectionDialogBase(BaseVersionSelectionDialog):
|
||||
"""
|
||||
Базовый класс для диалога выбора версии Wine.
|
||||
"""
|
||||
|
||||
def __init__(self, architecture, parent=None):
|
||||
"""
|
||||
Инициализирует диалог выбора версии Wine.
|
||||
"""
|
||||
self.architecture = architecture
|
||||
self.wine_versions_data = {}
|
||||
self.system_wine_display_name = "Системная версия"
|
||||
self.selected_display_text = None
|
||||
|
||||
super().__init__(
|
||||
title=f"Выбор версии Wine для {architecture} префикса",
|
||||
parent=parent,
|
||||
min_width=900,
|
||||
min_height=500
|
||||
)
|
||||
|
||||
def _create_version_container(self):
|
||||
"""Создает контейнер с вкладками для версий Wine."""
|
||||
self.main_tabs = QTabWidget()
|
||||
self.main_layout.addWidget(self.main_tabs)
|
||||
self.load_versions()
|
||||
|
||||
def load_versions(self):
|
||||
"""Загружает и отображает версии Wine."""
|
||||
self.main_tabs.clear()
|
||||
|
||||
loading_widget = QWidget()
|
||||
loading_layout = QVBoxLayout(loading_widget)
|
||||
status_label = QLabel("Загрузка, пожалуйста, подождите...")
|
||||
status_label.setAlignment(Qt.AlignCenter)
|
||||
loading_layout.addWidget(status_label)
|
||||
self.main_tabs.addTab(loading_widget, "Загрузка...")
|
||||
self.main_tabs.setEnabled(False)
|
||||
|
||||
self._parse_sha256_list()
|
||||
self.populate_ui()
|
||||
|
||||
self.main_tabs.setEnabled(True)
|
||||
|
||||
def _parse_sha256_list(self):
|
||||
"""Парсит sha256sum.list для получения списка версий Wine."""
|
||||
sha256_path = self._get_sha256_path()
|
||||
self.wine_versions_data = Sha256Parser.parse_wine_versions(sha256_path)
|
||||
|
||||
if not self.wine_versions_data:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"Ошибка",
|
||||
f"Файл с версиями не найден или пуст:\n{sha256_path}"
|
||||
)
|
||||
|
||||
def _get_installed_versions(self):
|
||||
"""Возвращает список локально установленных версий Wine."""
|
||||
dist_path = EnvironmentConfig.WH_DIST_DIR
|
||||
if not dist_path:
|
||||
return []
|
||||
return Sha256Parser.get_installed_versions(dist_path)
|
||||
|
||||
def _get_system_wine_version(self):
|
||||
"""Получает версию системного Wine."""
|
||||
if shutil.which('wine'):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['wine', '--version'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
encoding='utf-8'
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except (FileNotFoundError, subprocess.CalledProcessError) as e:
|
||||
print(f"Не удалось получить версию системного wine: {e}")
|
||||
return "Системная версия"
|
||||
|
||||
def _is_version_compatible(self, version_name):
|
||||
"""Проверяет совместимость версии с выбранной архитектурой."""
|
||||
is_win64_prefix = self.architecture == "win64"
|
||||
is_win32_prefix = self.architecture == "win32"
|
||||
re_32bit_version = re.compile(r'i[3-6]86|i586')
|
||||
|
||||
if is_win64_prefix and re_32bit_version.search(version_name):
|
||||
return False
|
||||
|
||||
if is_win32_prefix:
|
||||
if version_name == "system" or 'wow64' in version_name:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def populate_ui(self):
|
||||
"""Заполняет UI версиями Wine."""
|
||||
raise NotImplementedError("Подкласс должен реализовать populate_ui")
|
||||
|
||||
|
||||
class ComponentVersionSelectionDialogBase(BaseVersionSelectionDialog):
|
||||
"""
|
||||
Базовый класс для диалога выбора версии компонента (DXVK, VKD3D).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
component_group,
|
||||
title,
|
||||
parent=None,
|
||||
add_extra_options=True
|
||||
):
|
||||
"""
|
||||
Инициализирует диалог выбора версии компонента.
|
||||
"""
|
||||
self.component_group = component_group
|
||||
self.versions_data = []
|
||||
self.add_extra_options = add_extra_options
|
||||
|
||||
super().__init__(
|
||||
title=title,
|
||||
parent=parent,
|
||||
min_width=600,
|
||||
min_height=400
|
||||
)
|
||||
|
||||
def _create_version_container(self):
|
||||
"""Создает контейнер для версий компонента."""
|
||||
self.scroll_area = QScrollArea()
|
||||
self.scroll_area.setWidgetResizable(True)
|
||||
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
self.main_layout.addWidget(self.scroll_area)
|
||||
|
||||
scroll_content = QWidget()
|
||||
self.scroll_area.setWidget(scroll_content)
|
||||
|
||||
self.grid_layout = QGridLayout(scroll_content)
|
||||
self.grid_layout.setAlignment(Qt.AlignTop)
|
||||
|
||||
self.load_versions(start_row=0)
|
||||
|
||||
if self.add_extra_options:
|
||||
self._create_action_buttons()
|
||||
|
||||
def _create_action_buttons(self):
|
||||
"""Создает кнопки действий (Удалить, Отмена)."""
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
uninstall_btn = QPushButton("Удалить из префикса")
|
||||
uninstall_btn.setToolTip("Удаляет текущую установленную версию компонента из префикса.")
|
||||
uninstall_btn.clicked.connect(lambda: self.on_version_selected("none"))
|
||||
button_layout.addWidget(uninstall_btn)
|
||||
|
||||
button_layout.addStretch(1)
|
||||
|
||||
cancel_button = QPushButton("Отмена")
|
||||
cancel_button.clicked.connect(self.reject)
|
||||
button_layout.addWidget(cancel_button)
|
||||
|
||||
self.main_layout.addLayout(button_layout)
|
||||
|
||||
def load_versions(self, start_row=0):
|
||||
"""Загружает и отображает версии компонента."""
|
||||
self._parse_sha256_list()
|
||||
self.populate_ui(start_row)
|
||||
|
||||
def _parse_sha256_list(self):
|
||||
"""Парсит sha256sum.list для получения списка версий компонента."""
|
||||
sha256_path = self._get_sha256_path()
|
||||
self.versions_data = Sha256Parser.parse_component_versions(
|
||||
sha256_path,
|
||||
self.component_group
|
||||
)
|
||||
|
||||
def populate_ui(self, start_row):
|
||||
"""Заполняет UI версиями компонента."""
|
||||
raise NotImplementedError("Подкласс должен реализовать populate_ui")
|
||||
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
Диалог для выбора версии компонента (DXVK, VKD3D).
|
||||
|
||||
"""
|
||||
from functools import partial
|
||||
|
||||
from PyQt5.QtWidgets import (
|
||||
QPushButton
|
||||
)
|
||||
|
||||
from winehelperqt.dialogs.base_version_dialog import ComponentVersionSelectionDialogBase
|
||||
|
||||
|
||||
class ComponentVersionSelectionDialog(ComponentVersionSelectionDialogBase):
|
||||
"""Диалог для выбора версии компонента (DXVK, VKD3D)."""
|
||||
|
||||
def __init__(self, component_group, title, parent=None, add_extra_options=True):
|
||||
"""
|
||||
Инициализирует диалог выбора версии компонента.
|
||||
|
||||
Args:
|
||||
component_group: Группа компонента (dxvk, vkd3d)
|
||||
title: Заголовок диалога
|
||||
parent: Родительский виджет
|
||||
add_extra_options: Добавлять ли кнопки действий
|
||||
"""
|
||||
super().__init__(
|
||||
component_group=component_group,
|
||||
title=title,
|
||||
parent=parent,
|
||||
add_extra_options=add_extra_options
|
||||
)
|
||||
|
||||
def populate_ui(self, start_row):
|
||||
"""Заполняет UI кнопками версий."""
|
||||
versions = sorted(self.versions_data, reverse=True)
|
||||
row, col = start_row, 0
|
||||
for version_name in versions:
|
||||
btn = QPushButton(version_name)
|
||||
btn.clicked.connect(partial(self.on_version_selected, version_name))
|
||||
self.grid_layout.addWidget(btn, row, col)
|
||||
self.buttons.append(btn)
|
||||
col += 1
|
||||
if col >= self.NUM_COLUMNS:
|
||||
col = 0
|
||||
row += 1
|
||||
|
||||
def filter_versions(self):
|
||||
"""Фильтрует видимость кнопок версий и перестраивает сетку."""
|
||||
search_text = self.search_edit.text().lower()
|
||||
|
||||
visible_buttons = []
|
||||
for btn_widget in self.buttons:
|
||||
if search_text in btn_widget.text().lower():
|
||||
visible_buttons.append(btn_widget)
|
||||
|
||||
for btn_widget in self.buttons:
|
||||
btn_widget.setVisible(False)
|
||||
|
||||
for i, btn_widget in enumerate(visible_buttons):
|
||||
row, col = divmod(i, self.NUM_COLUMNS)
|
||||
self.grid_layout.addWidget(btn_widget, row, col)
|
||||
btn_widget.setVisible(True)
|
||||
|
||||
def on_version_selected(self, version_name):
|
||||
"""Обрабатывает выбор версии."""
|
||||
self.selected_version = version_name
|
||||
self.accept()
|
||||
@@ -0,0 +1,202 @@
|
||||
"""
|
||||
Диалог для создания нового префикса.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QPushButton,
|
||||
QLabel, QLineEdit, QRadioButton, QComboBox, QMessageBox, QWidget
|
||||
)
|
||||
from PyQt5.QtGui import QFont
|
||||
|
||||
from winehelperqt.parsers.sha256_parser import Sha256Parser
|
||||
from winehelperqt.dialogs.wine_version import WineVersionSelectionDialog
|
||||
from winehelperqt.utils.environment import EnvironmentConfig
|
||||
|
||||
|
||||
class CreatePrefixDialog(QDialog):
|
||||
"""Диалог для создания нового префикса."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.parent_gui = parent
|
||||
self.setWindowTitle("Создание нового префикса")
|
||||
self.setMinimumSize(680, 250)
|
||||
self.setModal(True)
|
||||
|
||||
self.prefix_name = None
|
||||
self.wine_arch = None
|
||||
self.base_pfx = None
|
||||
self.prepared_prefixes = {}
|
||||
self.selected_wine_version_value = None
|
||||
self.selected_wine_version_display = None
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
form_layout = QFormLayout()
|
||||
form_layout.setSpacing(10)
|
||||
|
||||
name_input_widget = QWidget()
|
||||
name_input_layout = QVBoxLayout(name_input_widget)
|
||||
name_input_layout.setContentsMargins(0, 0, 0, 0)
|
||||
name_input_layout.setSpacing(2)
|
||||
|
||||
self.prefix_name_edit = QLineEdit()
|
||||
self.prefix_name_edit.setPlaceholderText("Например: my_prefix")
|
||||
name_input_layout.addWidget(self.prefix_name_edit)
|
||||
|
||||
self.name_warning_label = QLabel(
|
||||
"Имя может содержать только латинские буквы, цифры, тире и знаки подчеркивания."
|
||||
)
|
||||
self.name_warning_label.setStyleSheet("color: red;")
|
||||
self.name_warning_label.setVisible(False)
|
||||
name_input_layout.addWidget(self.name_warning_label)
|
||||
|
||||
form_layout.addRow("<b>Имя нового префикса:</b>", name_input_widget)
|
||||
|
||||
arch_widget = QWidget()
|
||||
arch_layout = QHBoxLayout(arch_widget)
|
||||
arch_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.arch_win32_radio = QRadioButton("32-bit")
|
||||
self.arch_win64_radio = QRadioButton("64-bit")
|
||||
self.arch_win64_radio.setChecked(True)
|
||||
arch_layout.addWidget(self.arch_win32_radio)
|
||||
arch_layout.addWidget(self.arch_win64_radio)
|
||||
form_layout.addRow("<b>Разрядность:</b>", arch_widget)
|
||||
|
||||
self.prefix_template_selector = QComboBox()
|
||||
self.prefix_template_selector.setToolTip("Выберите шаблон для создания префикса.")
|
||||
form_layout.addRow("<b>Шаблон префикса:</b>", self.prefix_template_selector)
|
||||
|
||||
self.wine_version_edit = QLineEdit()
|
||||
self.wine_version_edit.setReadOnly(True)
|
||||
self.wine_version_edit.setPlaceholderText("Версия не выбрана")
|
||||
|
||||
select_version_button = QPushButton("Выбрать версию...")
|
||||
select_version_button.clicked.connect(self.open_wine_version_dialog)
|
||||
|
||||
version_layout = QHBoxLayout()
|
||||
version_layout.addWidget(self.wine_version_edit)
|
||||
version_layout.addWidget(select_version_button)
|
||||
form_layout.addRow("<b>Версия Wine:</b>", version_layout)
|
||||
|
||||
layout.addLayout(form_layout)
|
||||
|
||||
button_layout = QHBoxLayout()
|
||||
self.create_button = QPushButton("Создать")
|
||||
self.create_button.setFont(QFont('Arial', 11, QFont.Bold))
|
||||
self.create_button.setStyleSheet("background-color: #0078d7; color: white;")
|
||||
self.create_button.setEnabled(False)
|
||||
self.create_button.clicked.connect(self.accept_creation)
|
||||
|
||||
cancel_button = QPushButton("Отмена")
|
||||
cancel_button.clicked.connect(self.reject)
|
||||
|
||||
button_layout.addStretch()
|
||||
button_layout.addWidget(self.create_button)
|
||||
button_layout.addWidget(cancel_button)
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
self.arch_win32_radio.toggled.connect(self.on_architecture_changed)
|
||||
self.prefix_name_edit.textChanged.connect(self.validate_prefix_name)
|
||||
self.wine_version_edit.textChanged.connect(self.update_create_button_state)
|
||||
|
||||
self._load_prepared_prefixes()
|
||||
self.on_architecture_changed()
|
||||
|
||||
def _load_prepared_prefixes(self):
|
||||
"""Загружает и парсит шаблоны префиксов из sha256sum.list."""
|
||||
self.prepared_prefixes = {'win32': [], 'win64': []}
|
||||
data_path = EnvironmentConfig.DATA_PATH
|
||||
if not data_path:
|
||||
QMessageBox.warning(self, "Ошибка", "Переменная окружения DATA_PATH не установлена.")
|
||||
return
|
||||
sha256_file = os.path.join(data_path, "sha256sum.list")
|
||||
if not os.path.exists(sha256_file):
|
||||
QMessageBox.warning(self, "Ошибка", f"Файл с описаниями префиксов не найден: {sha256_file}")
|
||||
return
|
||||
|
||||
prepared = Sha256Parser.parse_prepared_prefixes(sha256_file)
|
||||
self.prepared_prefixes = prepared
|
||||
|
||||
def open_wine_version_dialog(self):
|
||||
"""Открывает диалог выбора версии Wine."""
|
||||
architecture = "win32" if self.arch_win32_radio.isChecked() else "win64"
|
||||
dialog = WineVersionSelectionDialog(architecture, self)
|
||||
if dialog.exec_() == QDialog.Accepted and dialog.selected_version:
|
||||
self.wine_version_edit.setText(dialog.selected_display_text)
|
||||
self.selected_wine_version_value = dialog.selected_version
|
||||
|
||||
def on_architecture_changed(self):
|
||||
"""Обновляет список шаблонов и сбрасывает выбор версии Wine."""
|
||||
self.wine_version_edit.clear()
|
||||
self.selected_wine_version_value = None
|
||||
|
||||
self.prefix_template_selector.blockSignals(True)
|
||||
self.prefix_template_selector.clear()
|
||||
|
||||
architecture = "win32" if self.arch_win32_radio.isChecked() else "win64"
|
||||
templates = self.prepared_prefixes.get(architecture, [])
|
||||
|
||||
for value, description in templates:
|
||||
display_name = "Чистый префикс" if value == "none" else value
|
||||
self.prefix_template_selector.addItem(display_name, userData={'value': value, 'tooltip': description})
|
||||
|
||||
self.prefix_template_selector.blockSignals(False)
|
||||
self.prefix_template_selector.currentIndexChanged.connect(
|
||||
lambda i: self.prefix_template_selector.setToolTip(
|
||||
self.prefix_template_selector.itemData(i)['tooltip']
|
||||
)
|
||||
)
|
||||
self.prefix_template_selector.setToolTip(self.prefix_template_selector.itemData(0)['tooltip'])
|
||||
|
||||
def validate_prefix_name(self, text):
|
||||
"""Проверяет имя префикса в реальном времени."""
|
||||
valid_pattern = r'^[a-zA-Z0-9_-]*$'
|
||||
if re.match(valid_pattern, text):
|
||||
self.name_warning_label.setVisible(False)
|
||||
else:
|
||||
cleaned_text = re.sub(r'[^a-zA-Z0-9_-]', '', text)
|
||||
self.prefix_name_edit.blockSignals(True)
|
||||
self.prefix_name_edit.setText(cleaned_text)
|
||||
self.prefix_name_edit.blockSignals(False)
|
||||
self.name_warning_label.setVisible(True)
|
||||
|
||||
self.update_create_button_state()
|
||||
|
||||
def update_create_button_state(self):
|
||||
"""Включает или выключает кнопку 'Создать'."""
|
||||
name_ok = bool(self.prefix_name_edit.text().strip())
|
||||
version_ok = bool(self.wine_version_edit.text().strip())
|
||||
self.create_button.setEnabled(
|
||||
name_ok and version_ok and not self.name_warning_label.isVisible()
|
||||
)
|
||||
|
||||
def accept_creation(self):
|
||||
"""Валидирует данные, сохраняет их и закрывает диалог с успехом."""
|
||||
prefix_name = self.prefix_name_edit.text().strip()
|
||||
|
||||
if not prefix_name:
|
||||
QMessageBox.warning(self, "Ошибка", "Имя префикса не может быть пустым.")
|
||||
return
|
||||
|
||||
if not re.match(r'^[a-zA-Z0-9_-]+$', prefix_name):
|
||||
QMessageBox.warning(
|
||||
self, "Ошибка",
|
||||
"Имя префикса может содержать только латинские буквы, цифры, дефисы и знаки подчеркивания."
|
||||
)
|
||||
return
|
||||
|
||||
prefix_path = os.path.join(EnvironmentConfig.USER_WORK_PATH, "prefixes", prefix_name)
|
||||
if os.path.exists(prefix_path):
|
||||
QMessageBox.warning(self, "Ошибка", f"Префикс с именем '{prefix_name}' уже существует.")
|
||||
return
|
||||
|
||||
self.prefix_name = prefix_name
|
||||
self.wine_arch = "win32" if self.arch_win32_radio.isChecked() else "win64"
|
||||
|
||||
current_index = self.prefix_template_selector.currentIndex()
|
||||
self.base_pfx = self.prefix_template_selector.itemData(current_index)['value']
|
||||
self.selected_wine_version_display = self.wine_version_edit.text()
|
||||
|
||||
self.accept()
|
||||
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
Диалог для управления ассоциациями файлов (WH_XDG_OPEN).
|
||||
"""
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QLineEdit, QLabel, QDialogButtonBox, QMessageBox
|
||||
)
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
|
||||
class FileAssociationsDialog(QDialog):
|
||||
"""Диалог для управления ассоциациями файлов."""
|
||||
|
||||
def __init__(self, current_associations, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Настройка ассоциаций файлов")
|
||||
self.setMinimumWidth(450)
|
||||
self.setModal(True)
|
||||
|
||||
self.new_associations = current_associations
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(10)
|
||||
|
||||
info_label = QLabel(
|
||||
"Укажите расширения файлов, которые должны открываться нативными<br>"
|
||||
"приложениями Linux. Чтобы удалить все ассоциации, очистите поле.<br><br>"
|
||||
"<b>Пример:</b> <code>pdf docx txt</code>"
|
||||
)
|
||||
info_label.setWordWrap(True)
|
||||
info_label.setTextFormat(Qt.RichText)
|
||||
layout.addWidget(info_label)
|
||||
|
||||
self.associations_edit = QLineEdit()
|
||||
if current_associations != "0":
|
||||
self.associations_edit.setText(current_associations)
|
||||
|
||||
self.associations_edit.setPlaceholderText("Введите расширения через пробел...")
|
||||
layout.addWidget(self.associations_edit)
|
||||
|
||||
forbidden_label = QLabel(
|
||||
"<small><b>Запрещено использовать:</b> cpl, dll, exe, lnk, msi</small>"
|
||||
)
|
||||
forbidden_label.setTextFormat(Qt.RichText)
|
||||
layout.addWidget(forbidden_label)
|
||||
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
button_box.accepted.connect(self.validate_and_accept)
|
||||
button_box.rejected.connect(self.reject)
|
||||
layout.addWidget(button_box)
|
||||
|
||||
def validate_and_accept(self):
|
||||
"""Проверяет введенные данные перед закрытием."""
|
||||
forbidden_extensions = {"cpl", "dll", "exe", "lnk", "msi"}
|
||||
|
||||
input_text = self.associations_edit.text().lower().strip()
|
||||
entered_extensions = {
|
||||
ext.strip() for ext in input_text.split() if ext.strip()
|
||||
}
|
||||
|
||||
found_forbidden = entered_extensions.intersection(forbidden_extensions)
|
||||
|
||||
if found_forbidden:
|
||||
msg_box = QMessageBox(self)
|
||||
msg_box.setIcon(QMessageBox.Warning)
|
||||
msg_box.setWindowTitle("Недопустимые расширения")
|
||||
msg_box.setTextFormat(Qt.RichText)
|
||||
msg_box.setText(
|
||||
"Следующие расширения запрещены и не могут быть использованы:<br><br>"
|
||||
f"<b>{', '.join(sorted(list(found_forbidden)))}</b>"
|
||||
)
|
||||
msg_box.exec_()
|
||||
return
|
||||
|
||||
self.new_associations = " ".join(sorted(list(entered_extensions)))
|
||||
self.accept()
|
||||
@@ -0,0 +1,356 @@
|
||||
"""
|
||||
Диалоги для дополнительных настроек префикса и приложений.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, QPushButton,
|
||||
QLabel, QCheckBox, QComboBox, QMessageBox, QSizePolicy
|
||||
)
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
|
||||
class AdditionalSettingsDialog(QDialog):
|
||||
"""Диалог для дополнительных настроек префикса (ESync, FSync, DXVK, VKD3D)."""
|
||||
|
||||
def __init__(self, prefix_name, parent=None):
|
||||
super().__init__(parent)
|
||||
self.parent_gui = parent
|
||||
self.prefix_name = prefix_name
|
||||
|
||||
self.setWindowTitle("Дополнительные настройки")
|
||||
self.setMinimumWidth(400)
|
||||
self.setModal(True)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
grid_layout = QGridLayout()
|
||||
grid_layout.setSpacing(10)
|
||||
|
||||
# ESync/FSync
|
||||
self.esync_button = QPushButton("ESync")
|
||||
self.esync_button.setCheckable(True)
|
||||
self.esync_button.setMinimumHeight(32)
|
||||
self.esync_button.setToolTip("Включить/выключить Eventfd-based synchronization.")
|
||||
self.esync_button.clicked.connect(
|
||||
lambda checked: self._toggle_sync_option("WINEESYNC", checked, self.esync_button)
|
||||
)
|
||||
self.esync_button.toggled.connect(
|
||||
lambda checked, btn=self.esync_button: self._update_button_text(btn, "ESync", checked)
|
||||
)
|
||||
self.esync_button.setStyleSheet("QPushButton:checked { background-color: #4CAF50; color: white; font-weight: bold; }")
|
||||
grid_layout.addWidget(self.esync_button, 0, 0)
|
||||
|
||||
self.fsync_button = QPushButton("FSync")
|
||||
self.fsync_button.setMinimumHeight(32)
|
||||
self.fsync_button.setCheckable(True)
|
||||
self.fsync_button.setToolTip("Включить/выключить Futex-based synchronization.")
|
||||
self.fsync_button.clicked.connect(
|
||||
lambda checked: self._toggle_sync_option("WINEFSYNC", checked, self.fsync_button)
|
||||
)
|
||||
self.fsync_button.toggled.connect(
|
||||
lambda checked, btn=self.fsync_button: self._update_button_text(btn, "FSync", checked)
|
||||
)
|
||||
self.fsync_button.setStyleSheet("QPushButton:checked { background-color: #4CAF50; color: white; font-weight: bold; }")
|
||||
grid_layout.addWidget(self.fsync_button, 0, 1)
|
||||
|
||||
self.esync_button.setFocusPolicy(Qt.NoFocus)
|
||||
self.fsync_button.setFocusPolicy(Qt.NoFocus)
|
||||
|
||||
# DXVK/VKD3D
|
||||
self.dxvk_manage_button = QPushButton("Управление DXVK")
|
||||
self.dxvk_manage_button.setMinimumHeight(32)
|
||||
self.dxvk_manage_button.clicked.connect(
|
||||
lambda: self._open_component_manager('dxvk')
|
||||
)
|
||||
self.dxvk_manage_button.setToolTip("Установка или удаление определенной версии DXVK в префиксе.")
|
||||
grid_layout.addWidget(self.dxvk_manage_button, 1, 0)
|
||||
|
||||
self.vkd3d_manage_button = QPushButton("Управление VKD3D")
|
||||
self.vkd3d_manage_button.setMinimumHeight(32)
|
||||
self.vkd3d_manage_button.clicked.connect(
|
||||
lambda: self._open_component_manager('vkd3d-proton')
|
||||
)
|
||||
self.vkd3d_manage_button.setToolTip("Установка или удаление определенной версии vkd3d-proton в префиксе.")
|
||||
grid_layout.addWidget(self.vkd3d_manage_button, 1, 1)
|
||||
|
||||
self.dxvk_manage_button.setFocusPolicy(Qt.NoFocus)
|
||||
self.vkd3d_manage_button.setFocusPolicy(Qt.NoFocus)
|
||||
|
||||
# Ассоциации файлов
|
||||
self.file_associations_button = QPushButton("Ассоциации файлов")
|
||||
self.file_associations_button.setMinimumHeight(32)
|
||||
self.file_associations_button.clicked.connect(self.parent_gui.open_file_associations_manager)
|
||||
self.file_associations_button.setToolTip(
|
||||
"Настройка открытия определенных типов файлов с помощью нативных приложений Linux."
|
||||
)
|
||||
grid_layout.addWidget(self.file_associations_button, 2, 0)
|
||||
self.file_associations_button.setFocusPolicy(Qt.NoFocus)
|
||||
|
||||
# CPCSP_PROXY
|
||||
self.cpcsp_proxy_button = QPushButton("CPCSP_PROXY")
|
||||
self.cpcsp_proxy_button.setMinimumHeight(32)
|
||||
self.cpcsp_proxy_button.setCheckable(True)
|
||||
self.cpcsp_proxy_button.setToolTip("Включить/выключить использование CPCSP_PROXY.")
|
||||
self.cpcsp_proxy_button.clicked.connect(
|
||||
lambda checked: self._toggle_sync_option("WH_USE_CPCSP_PROXY", checked, self.cpcsp_proxy_button)
|
||||
)
|
||||
self.cpcsp_proxy_button.toggled.connect(
|
||||
lambda checked, btn=self.cpcsp_proxy_button: self._update_button_text(btn, "CPCSP_PROXY", checked)
|
||||
)
|
||||
self.cpcsp_proxy_button.setStyleSheet("QPushButton:checked { background-color: #4CAF50; color: white; font-weight: bold; }")
|
||||
grid_layout.addWidget(self.cpcsp_proxy_button, 2, 1)
|
||||
self.cpcsp_proxy_button.setFocusPolicy(Qt.NoFocus)
|
||||
|
||||
layout.addLayout(grid_layout)
|
||||
|
||||
self.close_button = QPushButton("Закрыть")
|
||||
self.close_button.setMinimumHeight(32)
|
||||
self.close_button.clicked.connect(self.accept)
|
||||
self.close_button.setFocusPolicy(Qt.NoFocus)
|
||||
layout.addWidget(self.close_button, alignment=Qt.AlignRight)
|
||||
|
||||
self.update_button_states()
|
||||
|
||||
def _toggle_sync_option(self, option_name, is_checked, button):
|
||||
"""Обновляет опцию синхронизации."""
|
||||
self.parent_gui.update_sync_option(option_name, is_checked)
|
||||
|
||||
def _update_button_text(self, button, base_text, is_checked):
|
||||
"""Обновляет текст кнопки, добавляя или убирая галочку."""
|
||||
if is_checked:
|
||||
button.setText(f"✓ {base_text}")
|
||||
else:
|
||||
button.setText(base_text)
|
||||
|
||||
def _open_component_manager(self, component_name):
|
||||
"""Открывает менеджер компонентов."""
|
||||
self.parent_gui.open_component_version_manager(component_name)
|
||||
|
||||
def update_button_states(self):
|
||||
"""Обновляет состояние кнопок на основе данных из last.conf."""
|
||||
esync_enabled = self.parent_gui._get_prefix_component_version(
|
||||
self.prefix_name, "WINEESYNC"
|
||||
) == "1"
|
||||
fsync_enabled = self.parent_gui._get_prefix_component_version(
|
||||
self.prefix_name, "WINEFSYNC"
|
||||
) == "1"
|
||||
cpcsp_proxy_enabled = self.parent_gui._get_prefix_component_version(
|
||||
self.prefix_name, "WH_USE_CPCSP_PROXY"
|
||||
) == "1"
|
||||
|
||||
self.esync_button.blockSignals(True)
|
||||
self.fsync_button.blockSignals(True)
|
||||
self.cpcsp_proxy_button.blockSignals(True)
|
||||
|
||||
self.esync_button.setChecked(esync_enabled)
|
||||
self.fsync_button.setChecked(fsync_enabled)
|
||||
self.cpcsp_proxy_button.setChecked(cpcsp_proxy_enabled)
|
||||
|
||||
self._update_button_text(self.esync_button, "ESync", esync_enabled)
|
||||
self._update_button_text(self.fsync_button, "FSync", fsync_enabled)
|
||||
self._update_button_text(self.cpcsp_proxy_button, "CPCSP_PROXY", cpcsp_proxy_enabled)
|
||||
|
||||
self.esync_button.blockSignals(False)
|
||||
self.fsync_button.blockSignals(False)
|
||||
self.cpcsp_proxy_button.blockSignals(False)
|
||||
|
||||
|
||||
class AppAdditionalSettingsDialog(QDialog):
|
||||
"""Диалог для дополнительных настроек приложения из .whdb файла."""
|
||||
|
||||
def __init__(self, exe_path, parent=None):
|
||||
super().__init__(parent)
|
||||
self.parent_gui = parent
|
||||
self.exe_path = exe_path
|
||||
self.whdb_path = f"{exe_path}.whdb"
|
||||
|
||||
self.settings = {
|
||||
'WH_MAIN_DECORATED': 'default',
|
||||
'WH_VIRTUAL_DESKTOP': 'default',
|
||||
'WH_FONT_SMOOTHING': '0',
|
||||
'WH_FONT_MSS_REPLACE': '0',
|
||||
'WINE_D3D_CONFIG': ''
|
||||
}
|
||||
|
||||
self.setWindowTitle("Дополнительные настройки приложения")
|
||||
self.setMinimumSize(380, 200)
|
||||
self.setModal(True)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
checkboxes_layout = QHBoxLayout()
|
||||
checkboxes_layout.setSpacing(20)
|
||||
|
||||
left_column = QVBoxLayout()
|
||||
left_column.setSpacing(10)
|
||||
|
||||
self.wh_main_decorated_checkbox = QCheckBox("MAIN_DECORATED")
|
||||
self.wh_main_decorated_checkbox.setToolTip("Управляет декорациями окон главного приложения")
|
||||
left_column.addWidget(self.wh_main_decorated_checkbox)
|
||||
|
||||
self.wh_font_smoothing_checkbox = QCheckBox("FONT_SMOOTHING")
|
||||
self.wh_font_smoothing_checkbox.setToolTip("Сглаживание шрифтов")
|
||||
left_column.addWidget(self.wh_font_smoothing_checkbox)
|
||||
|
||||
right_column = QVBoxLayout()
|
||||
right_column.setSpacing(10)
|
||||
|
||||
self.wh_virtual_desktop_checkbox = QCheckBox("VIRTUAL_DESKTOP")
|
||||
self.wh_virtual_desktop_checkbox.setToolTip("Виртуальный рабочий стол для приложения")
|
||||
right_column.addWidget(self.wh_virtual_desktop_checkbox)
|
||||
|
||||
self.wh_font_mss_replace_checkbox = QCheckBox("FONT_MSS_REPLACE")
|
||||
self.wh_font_mss_replace_checkbox.setToolTip("Замена шрифтов MSS")
|
||||
right_column.addWidget(self.wh_font_mss_replace_checkbox)
|
||||
|
||||
checkboxes_layout.addLayout(left_column)
|
||||
checkboxes_layout.addLayout(right_column)
|
||||
layout.addLayout(checkboxes_layout)
|
||||
|
||||
layout.addSpacing(15)
|
||||
d3d_layout = QHBoxLayout()
|
||||
d3d_label = QLabel("WINE_D3D_CONFIG:")
|
||||
d3d_layout.addWidget(d3d_label)
|
||||
|
||||
self.wine_d3d_config_combo = QComboBox()
|
||||
self.wine_d3d_config_combo.setMinimumWidth(250)
|
||||
self.wine_d3d_config_combo.setSizePolicy(
|
||||
QSizePolicy.Preferred, QSizePolicy.Preferred
|
||||
)
|
||||
self.wine_d3d_config_combo.addItem("Не задано", "")
|
||||
self.wine_d3d_config_combo.addItem("renderer=vulkan", "renderer=vulkan")
|
||||
self.wine_d3d_config_combo.addItem("renderer=gdi", "renderer=gdi")
|
||||
self.wine_d3d_config_combo.addItem("renderer=gl", "renderer=gl")
|
||||
self.wine_d3d_config_combo.setToolTip("Выберите режим рендеринга Direct3D")
|
||||
d3d_layout.addWidget(self.wine_d3d_config_combo, stretch=1)
|
||||
d3d_layout.addStretch()
|
||||
layout.addLayout(d3d_layout)
|
||||
|
||||
layout.addSpacing(20)
|
||||
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.addStretch()
|
||||
|
||||
self.save_button = QPushButton("Сохранить")
|
||||
self.save_button.setMinimumHeight(32)
|
||||
self.save_button.clicked.connect(self._save_settings)
|
||||
self.save_button.setFocusPolicy(Qt.NoFocus)
|
||||
button_layout.addWidget(self.save_button)
|
||||
|
||||
self.close_button = QPushButton("Закрыть")
|
||||
self.close_button.setMinimumHeight(32)
|
||||
self.close_button.clicked.connect(self.accept)
|
||||
self.close_button.setFocusPolicy(Qt.NoFocus)
|
||||
button_layout.addWidget(self.close_button)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
self._load_settings()
|
||||
|
||||
def _load_settings(self):
|
||||
"""Загружает настройки из .whdb файла."""
|
||||
if not os.path.exists(self.whdb_path):
|
||||
self._update_ui_from_settings()
|
||||
return
|
||||
|
||||
try:
|
||||
with open(self.whdb_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
for line in content.split('\n'):
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
|
||||
match = re.match(r'export\s+(\w+)="([^"]*)"', line)
|
||||
if match:
|
||||
var_name = match.group(1)
|
||||
var_value = match.group(2)
|
||||
|
||||
if var_name in self.settings:
|
||||
self.settings[var_name] = var_value
|
||||
|
||||
self._update_ui_from_settings()
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.warning(
|
||||
self, "Ошибка",
|
||||
f"Не удалось прочитать файл настроек:\n{self.whdb_path}\n\nОшибка: {e}"
|
||||
)
|
||||
self._update_ui_from_settings()
|
||||
|
||||
def _update_ui_from_settings(self):
|
||||
"""Обновляет элементы управления на основе загруженных настроек."""
|
||||
self.wh_main_decorated_checkbox.setChecked(
|
||||
self.settings.get('WH_MAIN_DECORATED') == '1'
|
||||
)
|
||||
self.wh_virtual_desktop_checkbox.setChecked(
|
||||
self.settings.get('WH_VIRTUAL_DESKTOP') == '1' or
|
||||
(self.settings.get('WH_VIRTUAL_DESKTOP', '').lower() not in ('0', 'default', ''))
|
||||
)
|
||||
self.wh_font_smoothing_checkbox.setChecked(
|
||||
self.settings.get('WH_FONT_SMOOTHING') == '1'
|
||||
)
|
||||
self.wh_font_mss_replace_checkbox.setChecked(
|
||||
self.settings.get('WH_FONT_MSS_REPLACE') == '1'
|
||||
)
|
||||
|
||||
d3d_value = self.settings.get('WINE_D3D_CONFIG', '')
|
||||
index = self.wine_d3d_config_combo.findData(d3d_value)
|
||||
if index >= 0:
|
||||
self.wine_d3d_config_combo.setCurrentIndex(index)
|
||||
else:
|
||||
self.wine_d3d_config_combo.setCurrentIndex(0)
|
||||
|
||||
def _save_settings(self):
|
||||
"""Сохраняет настройки в .whdb файл."""
|
||||
try:
|
||||
existing_lines = []
|
||||
if os.path.exists(self.whdb_path):
|
||||
with open(self.whdb_path, 'r', encoding='utf-8') as f:
|
||||
existing_lines = f.readlines()
|
||||
|
||||
updated_vars = set()
|
||||
new_lines = []
|
||||
|
||||
for line in existing_lines:
|
||||
stripped = line.strip()
|
||||
match = re.match(r'export\s+(\w+)="([^"]*)"', stripped)
|
||||
if match:
|
||||
var_name = match.group(1)
|
||||
if var_name in self.settings:
|
||||
new_lines.append(self._create_export_line(var_name))
|
||||
updated_vars.add(var_name)
|
||||
continue
|
||||
new_lines.append(line)
|
||||
|
||||
for var_name in self.settings:
|
||||
if var_name not in updated_vars:
|
||||
new_lines.append(self._create_export_line(var_name))
|
||||
updated_vars.add(var_name)
|
||||
|
||||
with open(self.whdb_path, 'w', encoding='utf-8') as f:
|
||||
f.writelines(new_lines)
|
||||
|
||||
self.accept()
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить настройки:\n{e}")
|
||||
|
||||
def _create_export_line(self, var_name):
|
||||
"""Создает строку экспорта переменной."""
|
||||
if var_name == 'WINE_D3D_CONFIG':
|
||||
value = self.wine_d3d_config_combo.currentData()
|
||||
elif var_name == 'WH_MAIN_DECORATED':
|
||||
value = '1' if self.wh_main_decorated_checkbox.isChecked() else 'default'
|
||||
elif var_name == 'WH_VIRTUAL_DESKTOP':
|
||||
value = '1' if self.wh_virtual_desktop_checkbox.isChecked() else 'default'
|
||||
elif var_name == 'WH_FONT_SMOOTHING':
|
||||
value = '1' if self.wh_font_smoothing_checkbox.isChecked() else '0'
|
||||
elif var_name == 'WH_FONT_MSS_REPLACE':
|
||||
value = '1' if self.wh_font_mss_replace_checkbox.isChecked() else '0'
|
||||
else:
|
||||
value = ''
|
||||
|
||||
return f'export {var_name}="{value}"\n'
|
||||
@@ -0,0 +1,219 @@
|
||||
"""
|
||||
Диалог для выбора версии Wine с группировкой.
|
||||
"""
|
||||
import re
|
||||
from functools import partial
|
||||
|
||||
from PyQt5.QtWidgets import (
|
||||
QGridLayout, QScrollArea, QTabWidget, QPushButton, QLabel, QWidget, QVBoxLayout
|
||||
)
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
from winehelperqt.dialogs.base_version_dialog import WineVersionSelectionDialogBase
|
||||
|
||||
|
||||
class WineVersionSelectionDialog(WineVersionSelectionDialogBase):
|
||||
"""Диалог для выбора версии Wine с группировкой."""
|
||||
|
||||
def __init__(self, architecture, parent=None):
|
||||
"""
|
||||
Инициализирует диалог выбора версии Wine.
|
||||
|
||||
Args:
|
||||
architecture: Архитектура префикса (win32/win64)
|
||||
parent: Родительский виджет
|
||||
"""
|
||||
self.main_tabs = None
|
||||
self.installed_grid_layout = None
|
||||
self.installed_buttons = []
|
||||
self.download_buttons = []
|
||||
|
||||
super().__init__(architecture=architecture, parent=parent)
|
||||
|
||||
def _create_version_container(self):
|
||||
"""Создает контейнер с вкладками для версий Wine."""
|
||||
self.main_tabs = QTabWidget()
|
||||
self.main_layout.addWidget(self.main_tabs)
|
||||
self.load_versions()
|
||||
|
||||
def populate_ui(self):
|
||||
"""Заполняет UI версиями Wine."""
|
||||
self.main_tabs.clear()
|
||||
|
||||
# Вкладка "Установленные версии Wine"
|
||||
installed_tab = QWidget()
|
||||
self.main_tabs.addTab(installed_tab, "Установленные версии Wine")
|
||||
installed_layout = QVBoxLayout(installed_tab)
|
||||
installed_scroll_area = QScrollArea()
|
||||
installed_scroll_area.setWidgetResizable(True)
|
||||
installed_scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
installed_layout.addWidget(installed_scroll_area)
|
||||
installed_content = QWidget()
|
||||
installed_scroll_area.setWidget(installed_content)
|
||||
self.installed_grid_layout = QGridLayout(installed_content)
|
||||
self.installed_grid_layout.setAlignment(Qt.AlignTop)
|
||||
|
||||
installed_versions_for_grid = []
|
||||
|
||||
# Системная версия
|
||||
self.system_wine_display_name = self._get_system_wine_version()
|
||||
installed_versions_for_grid.append((self.system_wine_display_name, "system"))
|
||||
|
||||
# Локально установленные версии
|
||||
local_versions = self._get_installed_versions()
|
||||
installed_versions_for_grid.extend(local_versions)
|
||||
|
||||
self._create_grid_buttons_for_wine(self.installed_grid_layout, installed_versions_for_grid, is_download_tab=False)
|
||||
|
||||
# Вкладки с версиями для скачивания
|
||||
if not self.wine_versions_data:
|
||||
error_widget = QWidget()
|
||||
error_layout = QVBoxLayout(error_widget)
|
||||
error_label = QLabel("Список версий пуст или не удалось его загрузить.")
|
||||
error_label.setAlignment(Qt.AlignCenter)
|
||||
error_layout.addWidget(error_label)
|
||||
self.main_tabs.addTab(error_widget, "Ошибка")
|
||||
self.filter_versions()
|
||||
return
|
||||
|
||||
# Фильтрация и сортировка групп в зависимости от архитектуры
|
||||
is_win64 = self.architecture == "win64"
|
||||
|
||||
if is_win64:
|
||||
allowed_groups_for_arch = {"WINE AMD64", "WINE WOW64"}
|
||||
tab_order = ["WINE WOW64", "WINE AMD64"]
|
||||
display_names = {
|
||||
"WINE WOW64": "Загрузить wine-wow64",
|
||||
"WINE AMD64": "Загрузить wine-amd64"
|
||||
}
|
||||
else:
|
||||
allowed_groups_for_arch = {"WINE I586", "WINE AMD64"}
|
||||
tab_order = ["WINE I586", "WINE AMD64"]
|
||||
display_names = {
|
||||
"WINE I586": "Загрузить wine-i586",
|
||||
"WINE AMD64": "Загрузить wine-amd64"
|
||||
}
|
||||
|
||||
available_keys = [
|
||||
key for key in self.wine_versions_data.keys()
|
||||
if key in allowed_groups_for_arch
|
||||
]
|
||||
group_keys = sorted(available_keys, key=lambda k: tab_order.index(k))
|
||||
|
||||
for key in group_keys:
|
||||
versions = self.wine_versions_data.get(key, [])
|
||||
if not versions:
|
||||
continue
|
||||
display_name = display_names.get(key, key)
|
||||
self._create_version_tab_directly(display_name, sorted(versions, reverse=True))
|
||||
|
||||
self.filter_versions()
|
||||
|
||||
def _create_version_tab_directly(self, title, versions_list):
|
||||
"""Создает вкладку с сеткой кнопок и добавляет её напрямую в main_tabs."""
|
||||
tab_page = QWidget()
|
||||
tab_layout = QVBoxLayout(tab_page)
|
||||
tab_layout.setContentsMargins(5, 5, 5, 5)
|
||||
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True)
|
||||
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
tab_layout.addWidget(scroll_area)
|
||||
|
||||
scroll_content = QWidget()
|
||||
scroll_area.setWidget(scroll_content)
|
||||
|
||||
grid_layout = QGridLayout(scroll_content)
|
||||
grid_layout.setAlignment(Qt.AlignTop)
|
||||
|
||||
self._create_grid_buttons_for_wine(grid_layout, versions_list, is_download_tab=True)
|
||||
self.main_tabs.addTab(tab_page, title)
|
||||
|
||||
def _create_grid_buttons_for_wine(self, grid_layout, versions_list, is_download_tab=False):
|
||||
"""Создает кнопки для версий в сетке."""
|
||||
is_win64_prefix = self.architecture == "win64"
|
||||
is_win32_prefix = self.architecture == "win32"
|
||||
re_32bit_version = re.compile(r'i[3-6]86|i586')
|
||||
|
||||
row, col = 0, 0
|
||||
for version_data in versions_list:
|
||||
if isinstance(version_data, tuple):
|
||||
display_name, value_name = version_data
|
||||
else:
|
||||
display_name = value_name = version_data
|
||||
|
||||
btn = QPushButton(display_name)
|
||||
btn.clicked.connect(partial(self.on_version_selected, value_name, display_name))
|
||||
|
||||
if value_name == "system":
|
||||
btn.setText(f"{display_name} (Системный)")
|
||||
btn.setToolTip("Системная версия Wine, установленная в вашей ОС.")
|
||||
|
||||
# Отключение 32-битных версий для 64-битного префикса
|
||||
if not is_download_tab and is_win64_prefix:
|
||||
if re_32bit_version.search(value_name):
|
||||
btn.setEnabled(False)
|
||||
btn.setToolTip("Эта 32-битная версия Wine несовместима с 64-битным префиксом.")
|
||||
|
||||
# Отключение 64-битных версий для 32-битного префикса
|
||||
if not is_download_tab and is_win32_prefix:
|
||||
if value_name == "system" or 'wow64' in value_name:
|
||||
btn.setEnabled(False)
|
||||
btn.setToolTip("Эта 64-битная версия Wine несовместима с 32-битным префиксом.")
|
||||
|
||||
grid_layout.addWidget(btn, row, col)
|
||||
# Сохраняем для фильтрации
|
||||
if is_download_tab:
|
||||
self.download_buttons.append(btn)
|
||||
else:
|
||||
self.installed_buttons.append(btn)
|
||||
col += 1
|
||||
if col >= self.NUM_COLUMNS:
|
||||
col = 0
|
||||
row += 1
|
||||
|
||||
def filter_versions(self):
|
||||
"""Фильтрует видимость кнопок версий на основе текста поиска."""
|
||||
search_text = self.search_edit.text().lower()
|
||||
|
||||
# Фильтр для вкладки "Установленные"
|
||||
if hasattr(self, 'installed_grid_layout'):
|
||||
self._filter_grid(self.installed_grid_layout, search_text)
|
||||
|
||||
# Фильтр для вкладок с версиями (теперь напрямую в main_tabs)
|
||||
# Пропускаем первую вкладку "Установленные" (индекс 0)
|
||||
for i in range(1, self.main_tabs.count()):
|
||||
tab_widget = self.main_tabs.widget(i)
|
||||
# Ищем QGridLayout внутри вкладки
|
||||
scroll_area = tab_widget.findChild(QScrollArea)
|
||||
if scroll_area:
|
||||
scroll_content = scroll_area.widget()
|
||||
if scroll_content:
|
||||
grid_layout = scroll_content.layout()
|
||||
if grid_layout:
|
||||
any_visible = self._filter_grid(grid_layout, search_text)
|
||||
self.main_tabs.setTabEnabled(i, any_visible)
|
||||
|
||||
def _filter_grid(self, grid_layout, search_text):
|
||||
"""Helper-функция для фильтрации кнопок в одной сетке."""
|
||||
any_visible_in_grid = False
|
||||
if not grid_layout:
|
||||
return False
|
||||
|
||||
for j in range(grid_layout.count()):
|
||||
item = grid_layout.itemAt(j)
|
||||
if item:
|
||||
btn_widget = item.widget()
|
||||
if isinstance(btn_widget, QPushButton):
|
||||
is_match = search_text in btn_widget.text().lower()
|
||||
btn_widget.setVisible(is_match)
|
||||
if is_match:
|
||||
any_visible_in_grid = True
|
||||
|
||||
return any_visible_in_grid
|
||||
|
||||
def on_version_selected(self, version_name, display_name=None):
|
||||
"""Обрабатывает выбор версии Wine."""
|
||||
self.selected_version = version_name
|
||||
self.selected_display_text = display_name or version_name
|
||||
self.accept()
|
||||
@@ -0,0 +1,599 @@
|
||||
"""
|
||||
Диалог для управления компонентами Winetricks.
|
||||
"""
|
||||
import os
|
||||
import shlex
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
from functools import partial
|
||||
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QTabWidget,
|
||||
QTextEdit, QListWidget, QListWidgetItem, QMessageBox, QLineEdit, QWidget
|
||||
)
|
||||
from PyQt5.QtCore import Qt, QProcess, QTimer, QProcessEnvironment, pyqtSignal
|
||||
from PyQt5.QtGui import QTextCursor, QTextCharFormat, QFont
|
||||
|
||||
from winehelperqt.parsers.winetricks_parser import WinetricksOutputParser
|
||||
from winehelperqt.services.winetricks_service import WinetricksLogReader
|
||||
from winehelperqt.utils.environment import EnvironmentConfig
|
||||
|
||||
|
||||
class NoAutoScrollListWidget(QListWidget):
|
||||
"""QListWidget с отключённой автопрокруткой к текущему элементу."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._scroll_enabled = False # Флаг для ручной прокрутки
|
||||
|
||||
def scrollTo(self, index, hint=QListWidget.EnsureVisible):
|
||||
"""Переопределяем, чтобы игнорировать автоматическую прокрутку."""
|
||||
if self._scroll_enabled:
|
||||
super().scrollTo(index, hint)
|
||||
|
||||
def enable_scroll(self):
|
||||
"""Включает прокрутку для ручного управления."""
|
||||
self._scroll_enabled = True
|
||||
|
||||
def disable_scroll(self):
|
||||
"""Отключает прокрутку."""
|
||||
self._scroll_enabled = False
|
||||
|
||||
|
||||
class WinetricksManagerDialog(QDialog):
|
||||
"""Диалог для управления компонентами Winetricks."""
|
||||
|
||||
INFO_TEXT = (
|
||||
"Компоненты можно только установить либо переустановить.\n"
|
||||
"Удаление компонентов не реализовано в Winetricks.\n"
|
||||
"Для установки нового компонента: Поставьте галочку и нажмите «Применить».\n"
|
||||
"Для переустановки компонента: Выделите его в списке и нажмите кнопку «Переустановить»."
|
||||
)
|
||||
|
||||
installation_complete = pyqtSignal()
|
||||
|
||||
def __init__(self, prefix_path, winetricks_path, parent=None, wine_executable=None):
|
||||
super().__init__(parent)
|
||||
self.prefix_path = prefix_path
|
||||
self.winetricks_path = winetricks_path
|
||||
self.wine_executable = wine_executable or 'wine'
|
||||
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.is_reloading_after_cache_clear = False
|
||||
self.cache_clear_process = None
|
||||
|
||||
self.setWindowTitle(f"Менеджер компонентов для префикса: {os.path.basename(prefix_path)}")
|
||||
self.setMinimumSize(800, 500)
|
||||
|
||||
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.clear_cache_button = QPushButton("Очистить кеш")
|
||||
self.clear_cache_button.setStyleSheet("background-color: red; color: white;")
|
||||
self.clear_cache_button.clicked.connect(self.clear_winetricks_cache)
|
||||
button_layout.addWidget(self.clear_cache_button)
|
||||
|
||||
button_layout.addStretch(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 = NoAutoScrollListWidget()
|
||||
list_widget.setAutoScroll(False) # Отключаем автопрокрутку
|
||||
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):
|
||||
"""Фильтрует элементы в списке."""
|
||||
# Блокируем все сигналы виджета, чтобы избежать автопрокрутки
|
||||
# и лишних вызовов _update_ui_state при скрытии элементов
|
||||
list_widget.blockSignals(True)
|
||||
list_widget.disable_scroll() # Отключаем прокрутку
|
||||
|
||||
for i in range(list_widget.count()):
|
||||
item = list_widget.item(i)
|
||||
item.setHidden(text.lower() not in item.text().lower())
|
||||
|
||||
# Включаем прокрутку и прокручиваем к первому видимому элементу
|
||||
list_widget.enable_scroll()
|
||||
for i in range(list_widget.count()):
|
||||
item = list_widget.item(i)
|
||||
if not item.isHidden():
|
||||
list_widget.scrollToItem(item, QListWidget.PositionAtTop)
|
||||
break
|
||||
|
||||
list_widget.blockSignals(False)
|
||||
|
||||
# Обновляем состояние кнопок после фильтрации
|
||||
self._update_ui_state()
|
||||
|
||||
def load_all_categories(self):
|
||||
"""Запускает загрузку всех категорий."""
|
||||
self.loading_count = len(self.categories)
|
||||
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
|
||||
|
||||
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)
|
||||
env.insert("WINE", self.wine_executable)
|
||||
process.setProcessEnvironment(env)
|
||||
|
||||
process.finished.connect(partial(self._on_load_finished, category))
|
||||
process.start(self.winetricks_path, [category, "list"])
|
||||
|
||||
def _parse_winetricks_log(self):
|
||||
"""Читает winetricks.log и возвращает множество установленных компонентов."""
|
||||
log_reader = WinetricksLogReader(self.prefix_path)
|
||||
return log_reader.get_installed_verbs()
|
||||
|
||||
def _parse_winetricks_list_output(self, output, installed_verbs, list_widget, category):
|
||||
"""Парсит вывод 'winetricks list' и заполняет QListWidget."""
|
||||
parser = WinetricksOutputParser()
|
||||
items, found_items = parser.parse_list_output(output, installed_verbs, category)
|
||||
|
||||
for item_data in items:
|
||||
item = QListWidgetItem(item_data.item_text)
|
||||
item.setData(Qt.UserRole, item_data.name)
|
||||
item.setFont(QFont("DejaVu Sans Mono", 10))
|
||||
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
||||
item.setCheckState(Qt.Checked if item_data.is_installed else Qt.Unchecked)
|
||||
list_widget.addItem(item)
|
||||
self.initial_states[item_data.name] = item_data.is_installed
|
||||
|
||||
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_display_name}' (код: {exit_code}) ---", "red")
|
||||
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:
|
||||
installed_verbs = self._parse_winetricks_log()
|
||||
found_items = self._parse_winetricks_list_output(output, installed_verbs, list_widget, category)
|
||||
|
||||
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("--------------------------------------------------")
|
||||
|
||||
has_errors = any(status == 'error' for status in self.category_statuses.values())
|
||||
|
||||
self.loading_count -= 1
|
||||
if self.loading_count == 0:
|
||||
self._update_ui_state()
|
||||
if self.is_reloading_after_cache_clear:
|
||||
if has_errors:
|
||||
self._log("\n=== Ошибка при обновлении списков. Проверьте лог выше. ===", "red")
|
||||
else:
|
||||
self._log("\n=== Списки успешно обновлены ===")
|
||||
self.is_reloading_after_cache_clear = False
|
||||
|
||||
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):
|
||||
"""Обновляет состояние кнопок 'Применить' и 'Переустановить'."""
|
||||
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)
|
||||
|
||||
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 = []
|
||||
verbs_to_reinstall = []
|
||||
|
||||
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:
|
||||
if initial_state:
|
||||
verbs_to_reinstall.append(name)
|
||||
else:
|
||||
verbs_to_install.append(name)
|
||||
|
||||
if not verbs_to_install and not verbs_to_reinstall:
|
||||
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, verbs_to_reinstall)
|
||||
|
||||
def _start_install_process(self, verbs_to_install, verbs_to_reinstall=None):
|
||||
"""Запускает процесс установки/переустановки winetricks.
|
||||
|
||||
Args:
|
||||
verbs_to_install: Список компонентов для новой установки (без --force)
|
||||
verbs_to_reinstall: Список компонентов для переустановки (с --force)
|
||||
"""
|
||||
if verbs_to_reinstall is None:
|
||||
verbs_to_reinstall = []
|
||||
|
||||
# Устанавливает новые компоненты (без --force)
|
||||
if verbs_to_install:
|
||||
self._log(f"Выполнение установки: winetricks --unattended {' '.join(verbs_to_install)}")
|
||||
self._run_winetricks_process(verbs_to_install, use_force=False)
|
||||
|
||||
# Переустанавливает компоненты (с --force)
|
||||
if verbs_to_reinstall:
|
||||
if verbs_to_install:
|
||||
# Если уже что-то устанавливается, добавляем небольшую задержку
|
||||
self._log("\n---\n")
|
||||
self._log(f"Выполнение переустановки: winetricks --unattended --force {' '.join(verbs_to_reinstall)}")
|
||||
self._run_winetricks_process(verbs_to_reinstall, use_force=True)
|
||||
|
||||
def _run_winetricks_process(self, verbs, use_force=False):
|
||||
"""Запускает процесс winetricks с указанными компонентами.
|
||||
|
||||
Args:
|
||||
verbs: Список компонентов для установки
|
||||
use_force: Использовать ли флаг --force
|
||||
"""
|
||||
args = ["--unattended"]
|
||||
if use_force:
|
||||
args.append("--force")
|
||||
args.extend(verbs)
|
||||
|
||||
if not self.apply_process:
|
||||
self.apply_process = QProcess(self)
|
||||
self.apply_process.setProcessChannelMode(QProcess.MergedChannels)
|
||||
env = QProcessEnvironment.systemEnvironment()
|
||||
env.insert("WINEPREFIX", self.prefix_path)
|
||||
env.insert("WINE", self.wine_executable)
|
||||
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, args)
|
||||
|
||||
def on_apply_finished(self, exit_code, exit_status):
|
||||
"""Обрабатывает завершение применения изменений."""
|
||||
if self.user_cancelled:
|
||||
self._log("\n=== Установка прервана пользователем. ===")
|
||||
self.close()
|
||||
return
|
||||
|
||||
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.log_output.setCurrentCharFormat(QTextCharFormat())
|
||||
self.apply_button.setEnabled(True)
|
||||
self.close_button.setEnabled(True)
|
||||
return
|
||||
|
||||
self._log("\n=== Все операции успешно завершены ===")
|
||||
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.initial_states.clear()
|
||||
self.load_all_categories()
|
||||
self.installation_complete.emit()
|
||||
self.installation_finished = True
|
||||
|
||||
def clear_winetricks_cache(self):
|
||||
"""Запускает очистку кэша Winetricks."""
|
||||
reply = self._show_message_box(
|
||||
"Очистка кэша Winetricks",
|
||||
"Вы собираетесь удалить все скачанные установщики и списки компонентов Winetricks.\n\n"
|
||||
"Это действие может потребоваться, если у вас возникают проблемы со скачиванием или установкой компонентов.\n\n"
|
||||
"Продолжить?",
|
||||
QMessageBox.Question,
|
||||
{"buttons": {"Да": QMessageBox.YesRole, "Нет": QMessageBox.NoRole}, "default": "Нет"}
|
||||
)
|
||||
|
||||
if reply != "Да":
|
||||
return
|
||||
|
||||
self.tabs.setEnabled(False)
|
||||
self.clear_cache_button.setEnabled(False)
|
||||
self.apply_button.setEnabled(False)
|
||||
self.reinstall_button.setEnabled(False)
|
||||
self.close_button.setEnabled(False)
|
||||
|
||||
self.log_output.clear()
|
||||
|
||||
self.cache_clear_process = QProcess(self)
|
||||
self.cache_clear_process.setProcessChannelMode(QProcess.MergedChannels)
|
||||
|
||||
def handle_output():
|
||||
output = self.cache_clear_process.readAll().data().decode('utf-8', 'ignore').strip()
|
||||
if output:
|
||||
self._log(output)
|
||||
|
||||
def handle_finish(exit_code, exit_status):
|
||||
if exit_code == 0:
|
||||
self.is_reloading_after_cache_clear = True
|
||||
self.category_statuses.clear()
|
||||
os.makedirs(self.cache_dir, exist_ok=True)
|
||||
self._log("Обновление списков...")
|
||||
self.initial_states.clear()
|
||||
self.load_all_categories()
|
||||
else:
|
||||
self._log(f"\n=== Ошибка (код: {exit_code}) ===", "red")
|
||||
|
||||
self.tabs.setEnabled(True)
|
||||
self.clear_cache_button.setEnabled(True)
|
||||
self.close_button.setEnabled(True)
|
||||
self._update_ui_state()
|
||||
|
||||
self.cache_clear_process.readyRead.connect(handle_output)
|
||||
self.cache_clear_process.finished.connect(handle_finish)
|
||||
|
||||
winehelper_path = self.parent().winehelper_path if hasattr(self.parent(), 'winehelper_path') else EnvironmentConfig.RUN_SCRIPT
|
||||
args = ["clear-winetricks-cache", "--force"]
|
||||
|
||||
self._log(f"Выполнение: {shlex.quote(winehelper_path)} {' '.join(args)}\n")
|
||||
self.cache_clear_process.start(winehelper_path, args)
|
||||
|
||||
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.log_output.append("\n=== Прерывание установки... Ожидание завершения процесса... ===")
|
||||
self.apply_process.terminate()
|
||||
event.ignore()
|
||||
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'<span style="color:{color};">{message}</span>')
|
||||
self.log_output.setCurrentCharFormat(QTextCharFormat())
|
||||
else:
|
||||
self.log_output.append(message)
|
||||
self.log_output.moveCursor(QTextCursor.End)
|
||||
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
Парсеры для winehelper.
|
||||
"""
|
||||
from winehelperqt.parsers.sha256_parser import Sha256Parser
|
||||
from winehelperqt.parsers.winetricks_parser import WinetricksOutputParser, WinetricksItem
|
||||
from winehelperqt.parsers.script_parser import ScriptParser
|
||||
|
||||
__all__ = [
|
||||
'Sha256Parser',
|
||||
'WinetricksOutputParser',
|
||||
'WinetricksItem',
|
||||
'ScriptParser',
|
||||
]
|
||||
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
Парсер для извлечения информации из скриптов установки.
|
||||
"""
|
||||
import shlex
|
||||
|
||||
|
||||
class ScriptParser:
|
||||
"""Утилитарный класс для парсинга информации из скриптов установки."""
|
||||
|
||||
@staticmethod
|
||||
def extract_icons_from_script(script_path):
|
||||
"""
|
||||
Извлекает иконку для скрипта.
|
||||
|
||||
Сначала ищет переменную 'export ICONS_BUTTON_GUI=', если не находит,
|
||||
то ищет 'export PROG_ICON=', затем ищет вызовы 'create_desktop'.
|
||||
|
||||
Returns:
|
||||
Список имен иконок
|
||||
"""
|
||||
try:
|
||||
with open(script_path, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
# Приоритет у ICONS_BUTTON_GUI
|
||||
for line in lines:
|
||||
if line.strip().startswith('export ICONS_BUTTON_GUI='):
|
||||
icon_names_str = line.split('=', 1)[1].strip().strip('"\'')
|
||||
if icon_names_str:
|
||||
return icon_names_str.split()
|
||||
|
||||
for line in lines:
|
||||
if line.strip().startswith('export PROG_ICON='):
|
||||
icon_names_str = line.split('=', 1)[1].strip().strip('"\'')
|
||||
if icon_names_str:
|
||||
return icon_names_str.split()
|
||||
|
||||
# Поиск в 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 and icon_name.lower() not in ('auto', 'nocopy'):
|
||||
icon_names.append(icon_name)
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
return icon_names
|
||||
except Exception as e:
|
||||
print(f"Ошибка чтения файла для извлечения иконки: {str(e)}")
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def extract_prog_name_from_script(script_path):
|
||||
"""Извлекает имя программы из строки PROG_NAME= в скрипте"""
|
||||
try:
|
||||
with open(script_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
def extract_prog_url_from_script(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
|
||||
|
||||
@staticmethod
|
||||
def extract_info_ru(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)}"
|
||||
@@ -0,0 +1,233 @@
|
||||
"""
|
||||
Парсер для файла sha256sum.list.
|
||||
Извлекает версии Wine и компонентов из файла хешей.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
class Sha256Parser:
|
||||
"""Парсер файла sha256sum.list для извлечения версий пакетов."""
|
||||
|
||||
WINE_ALLOWED_GROUPS = {"WINE WOW64", "WINE AMD64", "WINE I586"}
|
||||
GROUP_HEADER_PATTERN = re.compile(r'^#+\s*(.*?)\s*#*$')
|
||||
HASH_PATTERN = re.compile(r'^[a-f0-9]{64}')
|
||||
ARCHIVE_EXTENSION = '.tar.xz'
|
||||
|
||||
@classmethod
|
||||
def parse_wine_versions(cls, sha256_path):
|
||||
"""
|
||||
Парсит файл sha256sum.list и возвращает версии Wine, сгруппированные по архитектурам.
|
||||
|
||||
Args:
|
||||
sha256_path: Путь к файлу sha256sum.list
|
||||
|
||||
Returns:
|
||||
Словарь {группа: [версии]}, например:
|
||||
{"WINE AMD64": ["8.0", "7.0"], "WINE WOW64": ["8.0-wow64"]}
|
||||
"""
|
||||
versions_data = {}
|
||||
|
||||
if not os.path.exists(sha256_path):
|
||||
return versions_data
|
||||
|
||||
current_group = None
|
||||
|
||||
try:
|
||||
with open(sha256_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
match = cls.GROUP_HEADER_PATTERN.match(line)
|
||||
if match:
|
||||
group_name = match.group(1).strip()
|
||||
if group_name in cls.WINE_ALLOWED_GROUPS:
|
||||
current_group = group_name
|
||||
if current_group not in versions_data:
|
||||
versions_data[current_group] = []
|
||||
else:
|
||||
current_group = None
|
||||
continue
|
||||
|
||||
if current_group and cls.HASH_PATTERN.match(line):
|
||||
parts = line.split(maxsplit=1)
|
||||
if len(parts) == 2:
|
||||
filename = parts[1]
|
||||
if filename.endswith(cls.ARCHIVE_EXTENSION):
|
||||
version_name = filename[:-len(cls.ARCHIVE_EXTENSION)]
|
||||
versions_data[current_group].append(version_name)
|
||||
|
||||
except IOError:
|
||||
return {}
|
||||
|
||||
return versions_data
|
||||
|
||||
@classmethod
|
||||
def parse_component_versions(cls, sha256_path, component_group):
|
||||
"""
|
||||
Парсит файл sha256sum.list и возвращает версии конкретного компонента.
|
||||
|
||||
Args:
|
||||
sha256_path: Путь к файлу sha256sum.list
|
||||
component_group: Имя группы компонента (например, "DXVK", "VKD3D")
|
||||
|
||||
Returns:
|
||||
Список версий компонента, например: ["2.3", "2.2", "2.1"]
|
||||
"""
|
||||
versions_data = []
|
||||
|
||||
if not os.path.exists(sha256_path):
|
||||
return versions_data
|
||||
|
||||
current_group = None
|
||||
|
||||
try:
|
||||
with open(sha256_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
match = cls.GROUP_HEADER_PATTERN.match(line)
|
||||
if match:
|
||||
current_group = match.group(1).strip()
|
||||
continue
|
||||
|
||||
if current_group == component_group and cls.HASH_PATTERN.match(line):
|
||||
parts = line.split(maxsplit=1)
|
||||
if len(parts) == 2:
|
||||
filename = parts[1]
|
||||
if filename.endswith(cls.ARCHIVE_EXTENSION):
|
||||
version_name = filename[:-len(cls.ARCHIVE_EXTENSION)]
|
||||
versions_data.append(version_name)
|
||||
|
||||
except IOError:
|
||||
return []
|
||||
|
||||
return versions_data
|
||||
|
||||
@classmethod
|
||||
def parse_prepared_prefixes(cls, sha256_path):
|
||||
"""
|
||||
Парсит файл sha256sum.list и возвращает информацию о подготовленных префиксах.
|
||||
|
||||
Args:
|
||||
sha256_path: Путь к файлу sha256sum.list
|
||||
|
||||
Returns:
|
||||
Словарь {архитектура: [(имя_префикса, описание)]}, например:
|
||||
{'win32': [('prefix_x86_v1', 'Описание')], 'win64': [...]}
|
||||
"""
|
||||
prepared_prefixes = {'win32': [], 'win64': []}
|
||||
|
||||
if not os.path.exists(sha256_path):
|
||||
return prepared_prefixes
|
||||
|
||||
in_prefix_section = False
|
||||
current_description = ""
|
||||
current_prefix_name = None
|
||||
|
||||
try:
|
||||
with open(sha256_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
|
||||
if "##### PREFIX #####" in line:
|
||||
in_prefix_section = True
|
||||
continue
|
||||
elif line.startswith("#####"):
|
||||
if current_prefix_name and current_description:
|
||||
cls._add_prefix_to_list(
|
||||
prepared_prefixes,
|
||||
current_prefix_name,
|
||||
current_description
|
||||
)
|
||||
in_prefix_section = False
|
||||
current_description = ""
|
||||
current_prefix_name = None
|
||||
continue
|
||||
|
||||
if not in_prefix_section:
|
||||
continue
|
||||
|
||||
if cls.HASH_PATTERN.match(line):
|
||||
if current_prefix_name and current_description:
|
||||
cls._add_prefix_to_list(
|
||||
prepared_prefixes,
|
||||
current_prefix_name,
|
||||
current_description
|
||||
)
|
||||
|
||||
current_description = ""
|
||||
|
||||
parts = line.split(maxsplit=1)
|
||||
if len(parts) == 2:
|
||||
current_prefix_name = parts[1].replace('.tar.xz', '')
|
||||
else:
|
||||
current_prefix_name = None
|
||||
|
||||
elif line.startswith('# '):
|
||||
comment_line = line[2:].strip()
|
||||
if current_description:
|
||||
current_description += "\n" + comment_line
|
||||
else:
|
||||
current_description = comment_line
|
||||
|
||||
if current_prefix_name and current_description:
|
||||
cls._add_prefix_to_list(
|
||||
prepared_prefixes,
|
||||
current_prefix_name,
|
||||
current_description
|
||||
)
|
||||
|
||||
except IOError:
|
||||
return {'win32': [], 'win64': []}
|
||||
|
||||
cls._add_clean_prefix_option(prepared_prefixes)
|
||||
|
||||
return prepared_prefixes
|
||||
|
||||
@classmethod
|
||||
def _add_prefix_to_list(cls, prepared_prefixes, prefix_name, description):
|
||||
"""Добавляет префикс в список с определением архитектуры."""
|
||||
arch = None
|
||||
if "_x86_" in prefix_name or "_i586_" in prefix_name:
|
||||
arch = 'win32'
|
||||
elif "_x64_" in prefix_name or "_amd64_" in prefix_name:
|
||||
arch = 'win64'
|
||||
|
||||
if arch:
|
||||
prepared_prefixes[arch].append(
|
||||
(prefix_name, description.strip().replace('\\n', '\n'))
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _add_clean_prefix_option(cls, prepared_prefixes):
|
||||
"""Добавляет опцию 'Чистый префикс' для обеих архитектур."""
|
||||
clean_prefix_description = 'Создать чистый префикс без дополнительных библиотек'
|
||||
prepared_prefixes['win32'].insert(0, ('none', clean_prefix_description))
|
||||
prepared_prefixes['win64'].insert(0, ('none', clean_prefix_description))
|
||||
|
||||
@classmethod
|
||||
def get_installed_versions(cls, dist_path):
|
||||
"""
|
||||
Возвращает список локально установленных версий из директории dist.
|
||||
|
||||
Args:
|
||||
dist_path: Путь к директории с установленными версиями
|
||||
|
||||
Returns:
|
||||
Отсортированный список имен версий
|
||||
"""
|
||||
if not os.path.isdir(dist_path):
|
||||
return []
|
||||
|
||||
try:
|
||||
return sorted([
|
||||
name for name in os.listdir(dist_path)
|
||||
if os.path.isdir(os.path.join(dist_path, name))
|
||||
], reverse=True)
|
||||
except OSError:
|
||||
return []
|
||||
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
Парсер для вывода утилиты Winetricks.
|
||||
Извлекает списки компонентов и информацию об установленных пакетах.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
class WinetricksItem:
|
||||
"""Элемент списка компонентов Winetricks."""
|
||||
|
||||
def __init__(self, name, description, is_installed, item_text):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.is_installed = is_installed
|
||||
self.item_text = item_text
|
||||
|
||||
|
||||
class WinetricksOutputParser:
|
||||
"""Парсер вывода команды winetricks list."""
|
||||
|
||||
LINE_PATTERN = re.compile(r"^\s*(?:\[(.)]\s+)?([^\s]+)\s*(.*)")
|
||||
|
||||
DLLS_BLACKLIST_PATTERN = re.compile(
|
||||
r'^(dont_use|dxvk|vkd3d|galliumnine|faudio|foundation)',
|
||||
re.IGNORECASE
|
||||
)
|
||||
FONTS_BLACKLIST_PATTERN = re.compile(
|
||||
r'^(dont_use)',
|
||||
re.IGNORECASE
|
||||
)
|
||||
SETTINGS_BLACKLIST_PATTERN = re.compile(
|
||||
r'^(vista|alldlls|autostart_|bad|good|win|videomemory|vd=|isolate_home)',
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
DOWNLOAD_AVAILABLE_PATTERN = re.compile(r'\[\s*доступно для скачивания[^]]*\]')
|
||||
IN_CACHE_PATTERN = re.compile(r'\[\s*в кэше\s*\]')
|
||||
|
||||
SERVICE_WORDS = {'executing', 'using', 'warning:'}
|
||||
|
||||
@classmethod
|
||||
def parse_installed_verbs(cls, log_path):
|
||||
"""
|
||||
Читает winetricks.log и возвращает множество установленных компонентов.
|
||||
|
||||
Args:
|
||||
log_path: Путь к файлу winetricks.log
|
||||
|
||||
Returns:
|
||||
Множество имен установленных компонентов
|
||||
"""
|
||||
installed_verbs = set()
|
||||
|
||||
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:
|
||||
print(f"Предупреждение: не удалось прочитать {log_path}: {e}")
|
||||
|
||||
return installed_verbs
|
||||
|
||||
@classmethod
|
||||
def parse_list_output(cls, output, installed_verbs, category):
|
||||
"""
|
||||
Парсит вывод 'winetricks list' и возвращает список компонентов.
|
||||
|
||||
Args:
|
||||
output: Вывод команды winetricks list
|
||||
installed_verbs: Множество установленных компонентов
|
||||
category: Категория компонентов ('dlls', 'fonts', 'settings')
|
||||
|
||||
Returns:
|
||||
Кортеж (список элементов, найдены ли элементы)
|
||||
"""
|
||||
items = []
|
||||
found_items = False
|
||||
|
||||
for line in output.splitlines():
|
||||
match = cls.LINE_PATTERN.match(line)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
found_items = True
|
||||
_status, name, description = match.groups()
|
||||
|
||||
if not cls._is_valid_component(name, category):
|
||||
continue
|
||||
|
||||
description = cls._clean_description(description)
|
||||
|
||||
is_checked = name in installed_verbs
|
||||
item_text = f"{name.ljust(27)}{description.strip()}"
|
||||
|
||||
items.append(WinetricksItem(
|
||||
name=name,
|
||||
description=description.strip(),
|
||||
is_installed=is_checked,
|
||||
item_text=item_text
|
||||
))
|
||||
|
||||
return items, found_items
|
||||
|
||||
@classmethod
|
||||
def _get_blacklist_pattern(cls, category):
|
||||
"""Возвращает шаблон blacklist для указанной категории."""
|
||||
patterns = {
|
||||
'dlls': cls.DLLS_BLACKLIST_PATTERN,
|
||||
'fonts': cls.FONTS_BLACKLIST_PATTERN,
|
||||
'settings': cls.SETTINGS_BLACKLIST_PATTERN
|
||||
}
|
||||
return patterns.get(category)
|
||||
|
||||
@classmethod
|
||||
def _is_valid_component(cls, name, category):
|
||||
"""
|
||||
Проверяет, является ли имя допустимым компонентом.
|
||||
|
||||
Args:
|
||||
name: Имя компонента
|
||||
category: Категория компонента
|
||||
|
||||
Returns:
|
||||
True если компонент допустим, False иначе
|
||||
"""
|
||||
if '/' in name or '\\' in name:
|
||||
return False
|
||||
|
||||
if name.lower() in cls.SERVICE_WORDS or name.endswith(':'):
|
||||
return False
|
||||
|
||||
blacklist_pattern = cls._get_blacklist_pattern(category)
|
||||
if blacklist_pattern and blacklist_pattern.search(name):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def _clean_description(cls, description):
|
||||
"""
|
||||
Очищает описание от избыточной информации.
|
||||
|
||||
Args:
|
||||
description: Исходное описание
|
||||
|
||||
Returns:
|
||||
Очищенное описание
|
||||
"""
|
||||
description = cls.DOWNLOAD_AVAILABLE_PATTERN.sub('', description)
|
||||
description = cls.IN_CACHE_PATTERN.sub('', description)
|
||||
return description.strip()
|
||||
|
||||
@classmethod
|
||||
def get_blacklist_pattern_for_category(cls, category):
|
||||
"""
|
||||
Возвращает строковое представление blacklist паттерна для категории.
|
||||
Полезно для отладки и логирования.
|
||||
|
||||
Args:
|
||||
category: Категория ('dlls', 'fonts', 'settings')
|
||||
|
||||
Returns:
|
||||
Строковое представление паттерна или None
|
||||
"""
|
||||
patterns = {
|
||||
'dlls': cls.DLLS_BLACKLIST_PATTERN.pattern,
|
||||
'fonts': cls.FONTS_BLACKLIST_PATTERN.pattern,
|
||||
'settings': cls.SETTINGS_BLACKLIST_PATTERN.pattern
|
||||
}
|
||||
return patterns.get(category)
|
||||
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
Сервисы для winehelper GUI.
|
||||
"""
|
||||
from winehelperqt.services.winetricks_service import WinetricksCacheService, WinetricksLogReader
|
||||
from winehelperqt.services.prefix_manager import PrefixManager
|
||||
from winehelperqt.services.process_handler import ProcessHandler, CommandRunner
|
||||
from winehelperqt.services.desktop_file_manager import DesktopFileManager
|
||||
from winehelperqt.services.application_installer import ApplicationInstaller
|
||||
from winehelperqt.services.script_manager import ScriptManager, ScriptInfo
|
||||
from winehelperqt.services.installation_manager import InstallationManager
|
||||
from winehelperqt.services.app_launcher import AppLauncher
|
||||
from winehelperqt.services.settings_manager import SettingsManager
|
||||
|
||||
__all__ = [
|
||||
'WinetricksCacheService',
|
||||
'WinetricksLogReader',
|
||||
'PrefixManager',
|
||||
'ProcessHandler',
|
||||
'CommandRunner',
|
||||
'DesktopFileManager',
|
||||
'ApplicationInstaller',
|
||||
'ScriptManager',
|
||||
'ScriptInfo',
|
||||
'InstallationManager',
|
||||
'AppLauncher',
|
||||
'SettingsManager',
|
||||
]
|
||||
@@ -0,0 +1,293 @@
|
||||
"""
|
||||
Сервис для запуска и управления приложениями WineHelper.
|
||||
|
||||
Предоставляет функциональность:
|
||||
- Запуск приложений из .desktop файлов
|
||||
- Остановка приложений
|
||||
- Мониторинг запущенных приложений
|
||||
- Создание логов запуска (в обычном и debug режиме)
|
||||
"""
|
||||
import os
|
||||
import shlex
|
||||
from typing import Dict, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from PyQt5.QtCore import QProcess, QProcessEnvironment
|
||||
|
||||
from winehelperqt.utils.process_utils import ProcessUtils
|
||||
|
||||
|
||||
class AppLauncher:
|
||||
"""Сервис для запуска и управления приложениями Wine."""
|
||||
|
||||
def __init__(self, winehelper_path: str, user_work_path: str):
|
||||
"""
|
||||
Инициализирует лаунчер приложений.
|
||||
|
||||
Args:
|
||||
winehelper_path: Путь к скрипту winehelper
|
||||
user_work_path: Путь к рабочей директории пользователя
|
||||
"""
|
||||
self.winehelper_path = winehelper_path
|
||||
self.user_work_path = user_work_path
|
||||
self._active_processes: Dict[str, QProcess] = {}
|
||||
|
||||
def launch_app(self, desktop_path: str, debug: bool = False) -> QProcess:
|
||||
"""
|
||||
Запускает приложение из .desktop файла.
|
||||
|
||||
Args:
|
||||
desktop_path: Путь к .desktop файлу
|
||||
debug: Запустить в режиме отладки
|
||||
|
||||
Returns:
|
||||
QProcess запущенного приложения
|
||||
"""
|
||||
if not os.path.exists(desktop_path):
|
||||
raise FileNotFoundError(f"Desktop файл не найден: {desktop_path}")
|
||||
|
||||
# Парсим .desktop файл
|
||||
exec_command = self._get_exec_from_desktop(desktop_path)
|
||||
if not exec_command:
|
||||
raise ValueError(f"Не удалось получить команду запуска из: {desktop_path}")
|
||||
|
||||
process = QProcess()
|
||||
process.setProcessChannelMode(QProcess.MergedChannels)
|
||||
|
||||
# В режиме debug включаем полный вывод
|
||||
if debug:
|
||||
env = QProcessEnvironment.systemEnvironment()
|
||||
env.insert("WINEDEBUG", "+loaddll,+seh")
|
||||
process.setProcessEnvironment(env)
|
||||
|
||||
# Запускаем команду
|
||||
args = shlex.split(exec_command)
|
||||
executable = args[0]
|
||||
process.start(executable, args[1:])
|
||||
|
||||
if not process.waitForStarted():
|
||||
raise RuntimeError(f"Не удалось запустить приложение: {desktop_path}")
|
||||
|
||||
# Сохраняем процесс
|
||||
self._active_processes[desktop_path] = process
|
||||
|
||||
# Подключаем обработчик завершения
|
||||
process.finished.connect(
|
||||
lambda: self._on_app_finished(desktop_path, process)
|
||||
)
|
||||
|
||||
return process
|
||||
|
||||
def launch_app_with_log(self, desktop_path: str, debug: bool = False) -> QProcess:
|
||||
"""
|
||||
Запускает приложение с созданием лога.
|
||||
|
||||
Args:
|
||||
desktop_path: Путь к .desktop файлу
|
||||
debug: Создать debug лог
|
||||
|
||||
Returns:
|
||||
QProcess запущенного приложения
|
||||
"""
|
||||
process = self.launch_app(desktop_path, debug)
|
||||
|
||||
# Создаём лог если в debug режиме
|
||||
if debug:
|
||||
log_path = self._create_debug_log(desktop_path, process)
|
||||
if log_path:
|
||||
print(f"Debug лог создан: {log_path}")
|
||||
|
||||
return process
|
||||
|
||||
def launch_app_by_command(self, desktop_path: str,
|
||||
command: str,
|
||||
env_vars: Optional[Dict[str, str]] = None,
|
||||
debug: bool = False) -> QProcess:
|
||||
"""
|
||||
Запускает приложение по команде (для сложных случаев с env).
|
||||
|
||||
Args:
|
||||
desktop_path: Путь к .desktop файлу (для отслеживания)
|
||||
command: Команда запуска
|
||||
env_vars: Дополнительные переменные окружения
|
||||
debug: Режим отладки
|
||||
|
||||
Returns:
|
||||
QProcess запущенного приложения
|
||||
"""
|
||||
if desktop_path in self._active_processes:
|
||||
raise RuntimeError(f"Приложение уже запущено: {desktop_path}")
|
||||
|
||||
process = QProcess()
|
||||
process.setProcessChannelMode(QProcess.MergedChannels)
|
||||
|
||||
# Настраиваем окружение
|
||||
env = QProcessEnvironment.systemEnvironment()
|
||||
if env_vars:
|
||||
for key, value in env_vars.items():
|
||||
env.insert(key, value)
|
||||
if debug:
|
||||
env.insert("WINEDEBUG", "+loaddll,+seh")
|
||||
process.setProcessEnvironment(env)
|
||||
|
||||
# Парсим команду
|
||||
args = shlex.split(command)
|
||||
executable = args[0]
|
||||
process.start(executable, args[1:])
|
||||
|
||||
if not process.waitForStarted():
|
||||
raise RuntimeError(f"Не удалось запустить приложение: {desktop_path}")
|
||||
|
||||
self._active_processes[desktop_path] = process
|
||||
# НЕ подключаем обработчик — пусть вызывает сам
|
||||
|
||||
return process
|
||||
|
||||
def mark_app_finished(self, desktop_path: str) -> Optional[QProcess]:
|
||||
"""
|
||||
Отмечает приложение как завершённое и возвращает процесс.
|
||||
|
||||
Args:
|
||||
desktop_path: Путь к .desktop файлу
|
||||
|
||||
Returns:
|
||||
QProcess или None
|
||||
"""
|
||||
return self._active_processes.pop(desktop_path, None)
|
||||
|
||||
def stop_app(self, desktop_path: str) -> None:
|
||||
"""
|
||||
Останавливает запущенное приложение.
|
||||
|
||||
|
||||
Args:
|
||||
desktop_path: Путь к .desktop файлу
|
||||
"""
|
||||
process = self._active_processes.get(desktop_path)
|
||||
ProcessUtils.stop_process(process)
|
||||
|
||||
def is_app_running(self, desktop_path: str) -> bool:
|
||||
"""
|
||||
Проверяет, запущено ли приложение.
|
||||
|
||||
Args:
|
||||
desktop_path: Путь к .desktop файлу
|
||||
|
||||
Returns:
|
||||
True если приложение запущено
|
||||
"""
|
||||
process = self._active_processes.get(desktop_path)
|
||||
return process and process.state() == QProcess.Running
|
||||
|
||||
def get_running_apps(self) -> Dict[str, QProcess]:
|
||||
"""
|
||||
Возвращает список запущенных приложений.
|
||||
|
||||
Returns:
|
||||
Словарь {desktop_path: QProcess}
|
||||
"""
|
||||
return {
|
||||
path: proc for path, proc in self._active_processes.items()
|
||||
if proc.state() == QProcess.Running
|
||||
}
|
||||
|
||||
def get_process_for_app(self, desktop_path: str) -> Optional[QProcess]:
|
||||
"""
|
||||
Возвращает процесс для приложения.
|
||||
|
||||
Args:
|
||||
desktop_path: Путь к .desktop файлу
|
||||
|
||||
Returns:
|
||||
QProcess или None
|
||||
"""
|
||||
return self._active_processes.get(desktop_path)
|
||||
|
||||
def connect_output_callback(self, desktop_path: str,
|
||||
callback) -> None:
|
||||
"""
|
||||
Подключает callback для обработки вывода приложения.
|
||||
|
||||
Args:
|
||||
desktop_path: Путь к .desktop файлу
|
||||
callback: Функция для обработки вывода
|
||||
"""
|
||||
process = self._active_processes.get(desktop_path)
|
||||
if process:
|
||||
def read_output():
|
||||
output = process.readAllStandardOutput().data().decode('utf-8', 'ignore')
|
||||
if output:
|
||||
callback(output)
|
||||
|
||||
process.readyReadStandardOutput.connect(read_output)
|
||||
|
||||
def stop_all(self) -> None:
|
||||
"""Останавливает все запущенные приложения."""
|
||||
for desktop_path in list(self._active_processes.keys()):
|
||||
self.stop_app(desktop_path)
|
||||
|
||||
def _on_app_finished(self, desktop_path: str, process: QProcess) -> None:
|
||||
"""Обработчик завершения приложения."""
|
||||
self._active_processes.pop(desktop_path, None)
|
||||
|
||||
def _get_exec_from_desktop(self, desktop_path: str) -> Optional[str]:
|
||||
"""
|
||||
Получает команду запуска из .desktop файла.
|
||||
|
||||
Args:
|
||||
desktop_path: Путь к .desktop файлу
|
||||
|
||||
Returns:
|
||||
Команда запуска или None
|
||||
"""
|
||||
try:
|
||||
with open(desktop_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line.startswith('Exec='):
|
||||
# Убираем %F, %f, %U, %u (параметры desktop-файла)
|
||||
exec_cmd = line[5:].strip()
|
||||
exec_cmd = exec_cmd.replace('%F', '').replace('%f', '')
|
||||
exec_cmd = exec_cmd.replace('%U', '').replace('%u', '')
|
||||
return exec_cmd.strip()
|
||||
except (IOError, OSError) as e:
|
||||
print(f"Ошибка чтения {desktop_path}: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def _create_debug_log(self, desktop_path: str, process: QProcess) -> Optional[str]:
|
||||
"""
|
||||
Создаёт debug лог для приложения.
|
||||
|
||||
Args:
|
||||
desktop_path: Путь к .desktop файлу
|
||||
process: QProcess приложения
|
||||
|
||||
Returns:
|
||||
Путь к файлу лога или None
|
||||
"""
|
||||
try:
|
||||
# Создаём директорию для логов
|
||||
log_dir = os.path.join(self.user_work_path, "logs")
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
|
||||
# Формируем имя файла
|
||||
app_name = os.path.splitext(os.path.basename(desktop_path))[0]
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
log_filename = f"{app_name}_{timestamp}.log"
|
||||
log_path = os.path.join(log_dir, log_filename)
|
||||
|
||||
# Читаем вывод и записываем в лог
|
||||
output = process.readAllStandardOutput().data().decode('utf-8', 'ignore')
|
||||
|
||||
with open(log_path, 'w', encoding='utf-8') as f:
|
||||
f.write(f"Debug лог для: {desktop_path}\n")
|
||||
f.write(f"Время запуска: {timestamp}\n")
|
||||
f.write("=" * 80 + "\n\n")
|
||||
f.write(output)
|
||||
|
||||
return log_path
|
||||
|
||||
except (IOError, OSError) as e:
|
||||
print(f"Ошибка создания debug лога: {e}")
|
||||
return None
|
||||
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
Сервис для установки приложений Wine.
|
||||
"""
|
||||
from typing import List, Optional
|
||||
|
||||
from PyQt5.QtCore import QProcess, QProcessEnvironment
|
||||
|
||||
from winehelperqt.services.script_manager import ScriptManager, ScriptInfo
|
||||
|
||||
|
||||
class ApplicationInstaller:
|
||||
"""
|
||||
Сервис для установки приложений через Wine.
|
||||
"""
|
||||
|
||||
def __init__(self, data_path, user_work_path):
|
||||
"""
|
||||
Инициализирует установщик приложений.
|
||||
|
||||
Args:
|
||||
data_path: Путь к директории данных
|
||||
user_work_path: Путь к рабочей директории пользователя
|
||||
"""
|
||||
self.data_path = data_path
|
||||
self.user_work_path = user_work_path
|
||||
self._script_manager = ScriptManager(data_path)
|
||||
|
||||
def get_script_folders(self, folder_name):
|
||||
"""
|
||||
Возвращает список папок со скриптами.
|
||||
"""
|
||||
return self._script_manager.get_script_folders(folder_name)
|
||||
|
||||
def get_script_info(self, script_path):
|
||||
"""
|
||||
Получает информацию о скрипте установки.
|
||||
|
||||
Returns:
|
||||
Словарь с информацией о скрипте
|
||||
"""
|
||||
script_info: Optional[ScriptInfo] = self._script_manager.get_script_info(script_path)
|
||||
if script_info is None:
|
||||
return {}
|
||||
|
||||
return {
|
||||
'name': script_info.name,
|
||||
'url': script_info.url,
|
||||
'description': script_info.description,
|
||||
'icons': script_info.icons
|
||||
}
|
||||
|
||||
def list_available_apps(self, folder_name):
|
||||
"""
|
||||
Возвращает список доступных приложений.
|
||||
|
||||
Returns:
|
||||
Список словарей с информацией о приложениях
|
||||
"""
|
||||
scripts: List[ScriptInfo] = self._script_manager.list_available_apps(folder_name)
|
||||
apps = []
|
||||
|
||||
for script in scripts:
|
||||
apps.append({
|
||||
'name': script.name,
|
||||
'folder': script.folder,
|
||||
'script': script.script_path,
|
||||
'description': script.description,
|
||||
'icons': script.icons,
|
||||
'url': script.url
|
||||
})
|
||||
|
||||
return apps
|
||||
|
||||
def get_autoinstall_apps(self):
|
||||
"""Возвращает список приложений для автоустановки."""
|
||||
return self.list_available_apps(self._script_manager.AUTOINSTALL_DIR)
|
||||
|
||||
def get_manualinstall_apps(self):
|
||||
"""Возвращает список приложений для ручной установки."""
|
||||
return self.list_available_apps(self._script_manager.MANUALINSTALL_DIR)
|
||||
|
||||
def get_testinstall_apps(self):
|
||||
"""Возвращает список приложений для тестовой установки."""
|
||||
return self.list_available_apps(self._script_manager.TESTINSTALL_DIR)
|
||||
|
||||
def run_installation(self, script_path, winehelper_path, log_callback=None):
|
||||
"""
|
||||
Запускает установку приложения.
|
||||
|
||||
Args:
|
||||
script_path: Путь к скрипту установки
|
||||
winehelper_path: Путь к winehelper
|
||||
log_callback: Callback для логирования (опционально)
|
||||
|
||||
Returns:
|
||||
QProcess запущенного процесса
|
||||
"""
|
||||
process = QProcess()
|
||||
process.setProcessChannelMode(QProcess.MergedChannels)
|
||||
|
||||
env = QProcessEnvironment.systemEnvironment()
|
||||
env.insert("WINEHELPER_PATH", winehelper_path)
|
||||
process.setProcessEnvironment(env)
|
||||
|
||||
if log_callback:
|
||||
process.readyReadStandardOutput.connect(
|
||||
lambda: log_callback(process.readAllStandardOutput().data().decode('utf-8', 'ignore'))
|
||||
)
|
||||
|
||||
process.start(script_path)
|
||||
return process
|
||||
|
||||
def search_apps(self, apps, search_text):
|
||||
"""
|
||||
Ищет приложения по названию.
|
||||
|
||||
Args:
|
||||
apps: Список приложений (словари)
|
||||
search_text: Текст для поиска
|
||||
|
||||
Returns:
|
||||
Отфильтрованный список приложений
|
||||
"""
|
||||
if not search_text:
|
||||
return apps
|
||||
|
||||
search_lower = search_text.lower()
|
||||
return [
|
||||
app for app in apps
|
||||
if search_lower in app['name'].lower() or
|
||||
search_lower in app.get('description', '').lower()
|
||||
]
|
||||
@@ -0,0 +1,190 @@
|
||||
"""
|
||||
Сервис для управления .desktop файлами.
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
|
||||
|
||||
class DesktopFileManager:
|
||||
"""
|
||||
Сервис для управления .desktop файлами приложений.
|
||||
|
||||
Предоставляет функциональность:
|
||||
- Создание .desktop файлов
|
||||
- Чтение информации из .desktop файлов
|
||||
- Удаление .desktop файлов
|
||||
- Поиск установленных приложений
|
||||
"""
|
||||
|
||||
def __init__(self, user_work_path):
|
||||
"""
|
||||
Инициализирует менеджер .desktop файлов.
|
||||
|
||||
Args:
|
||||
user_work_path: Путь к рабочей директории пользователя
|
||||
"""
|
||||
self.user_work_path = user_work_path
|
||||
# .desktop файлы создаются прямо в USER_WORK_PATH (см. winehelper: create_desktop)
|
||||
self.desktop_dir = user_work_path
|
||||
|
||||
def get_desktop_file_path(self, app_name):
|
||||
"""
|
||||
Возвращает путь к .desktop файлу приложения.
|
||||
|
||||
Args:
|
||||
app_name: Имя приложения
|
||||
|
||||
Returns:
|
||||
Путь к .desktop файлу
|
||||
"""
|
||||
return os.path.join(self.desktop_dir, f"{app_name}.desktop")
|
||||
|
||||
def list_desktop_files(self):
|
||||
"""
|
||||
Возвращает список всех .desktop файлов.
|
||||
|
||||
Returns:
|
||||
Список имен .desktop файлов (без расширения)
|
||||
"""
|
||||
if not os.path.isdir(self.desktop_dir):
|
||||
return []
|
||||
|
||||
try:
|
||||
return [
|
||||
f.replace('.desktop', '') for f in os.listdir(self.desktop_dir)
|
||||
if f.endswith('.desktop')
|
||||
]
|
||||
except OSError:
|
||||
return []
|
||||
|
||||
def read_desktop_file(self, app_name):
|
||||
"""
|
||||
Читает .desktop файл и возвращает его содержимое.
|
||||
|
||||
Args:
|
||||
app_name: Имя приложения
|
||||
|
||||
Returns:
|
||||
Словарь с полями из .desktop файла
|
||||
"""
|
||||
desktop_path = self.get_desktop_file_path(app_name)
|
||||
|
||||
if not os.path.exists(desktop_path):
|
||||
return {}
|
||||
|
||||
result = {}
|
||||
try:
|
||||
with open(desktop_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#') or line.startswith('['):
|
||||
continue
|
||||
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
result[key.strip()] = value.strip()
|
||||
except (IOError, OSError):
|
||||
return {}
|
||||
|
||||
return result
|
||||
|
||||
def get_app_name_from_desktop(self, app_name):
|
||||
"""
|
||||
Получает имя приложения из .desktop файла.
|
||||
|
||||
Args:
|
||||
app_name: Имя приложения
|
||||
|
||||
Returns:
|
||||
Отображаемое имя приложения
|
||||
"""
|
||||
data = self.read_desktop_file(app_name)
|
||||
return data.get('Name', app_name)
|
||||
|
||||
def get_app_icon_from_desktop(self, app_name):
|
||||
"""
|
||||
Получает путь к иконке приложения.
|
||||
|
||||
Args:
|
||||
app_name: Имя приложения
|
||||
|
||||
Returns:
|
||||
Путь к иконке или None
|
||||
"""
|
||||
data = self.read_desktop_file(app_name)
|
||||
return data.get('Icon')
|
||||
|
||||
def delete_desktop_file(self, app_name):
|
||||
"""
|
||||
Удаляет .desktop файл приложения.
|
||||
|
||||
Args:
|
||||
app_name: Имя приложения
|
||||
|
||||
Returns:
|
||||
True если удаление успешно, False иначе
|
||||
"""
|
||||
desktop_path = self.get_desktop_file_path(app_name)
|
||||
|
||||
if not os.path.exists(desktop_path):
|
||||
return False, f"Файл '{desktop_path}' не найден"
|
||||
|
||||
try:
|
||||
os.remove(desktop_path)
|
||||
return True, "Файл удален"
|
||||
except OSError as e:
|
||||
return False, f"Ошибка удаления: {e}"
|
||||
|
||||
def desktop_file_exists(self, app_name):
|
||||
"""
|
||||
Проверяет существование .desktop файла.
|
||||
|
||||
Args:
|
||||
app_name: Имя приложения
|
||||
|
||||
Returns:
|
||||
True если файл существует, False иначе
|
||||
"""
|
||||
return os.path.exists(self.get_desktop_file_path(app_name))
|
||||
|
||||
def get_installed_apps_with_info(self):
|
||||
"""
|
||||
Возвращает информацию обо всех установленных приложениях.
|
||||
|
||||
Returns:
|
||||
Список словарей с информацией о приложениях
|
||||
"""
|
||||
apps = []
|
||||
for app_name in self.list_desktop_files():
|
||||
data = self.read_desktop_file(app_name)
|
||||
apps.append({
|
||||
'name': app_name,
|
||||
'display_name': data.get('Name', app_name),
|
||||
'icon': data.get('Icon'),
|
||||
'exec': data.get('Exec'),
|
||||
'path': self.get_desktop_file_path(app_name)
|
||||
})
|
||||
return apps
|
||||
|
||||
def copy_desktop_file(self, source_path, dest_name):
|
||||
"""
|
||||
Копирует .desktop файл в директорию пользователя.
|
||||
|
||||
Args:
|
||||
source_path: Путь к исходному файлу
|
||||
dest_name: Имя destination файла (без расширения)
|
||||
|
||||
Returns:
|
||||
True если копирование успешно, False иначе
|
||||
"""
|
||||
if not os.path.exists(source_path):
|
||||
return False, f"Исходный файл не найден: {source_path}"
|
||||
|
||||
os.makedirs(self.desktop_dir, exist_ok=True)
|
||||
dest_path = self.get_desktop_file_path(dest_name)
|
||||
|
||||
try:
|
||||
shutil.copy2(source_path, dest_path)
|
||||
return True, "Файл скопирован"
|
||||
except (IOError, OSError) as e:
|
||||
return False, f"Ошибка копирования: {e}"
|
||||
@@ -0,0 +1,180 @@
|
||||
"""
|
||||
Сервис для управления процессом установки приложений WineHelper.
|
||||
|
||||
Предоставляет функциональность:
|
||||
- Запуск процесса установки
|
||||
- Мониторинг прогресса установки
|
||||
- Обработка логов (stdout/stderr)
|
||||
- Управление процессами (остановка, завершение)
|
||||
- Отмена установки
|
||||
"""
|
||||
import os
|
||||
from typing import Dict, Optional, Callable
|
||||
|
||||
from PyQt5.QtCore import QProcess, QProcessEnvironment
|
||||
|
||||
from winehelperqt.utils.process_utils import ProcessUtils
|
||||
|
||||
|
||||
class InstallationManager:
|
||||
"""Сервис для управления процессом установки приложений."""
|
||||
|
||||
def __init__(self, winehelper_path: str):
|
||||
"""
|
||||
Инициализирует менеджер установки.
|
||||
|
||||
Args:
|
||||
winehelper_path: Путь к скрипту winehelper
|
||||
"""
|
||||
self.winehelper_path = winehelper_path
|
||||
self._active_processes: Dict[str, QProcess] = {}
|
||||
|
||||
def start_install(self, script_path: str,
|
||||
work_path: Optional[str] = None,
|
||||
env_vars: Optional[Dict[str, str]] = None) -> QProcess:
|
||||
"""
|
||||
Запускает процесс установки приложения.
|
||||
|
||||
Args:
|
||||
script_path: Путь к скрипту установки (install.sh)
|
||||
work_path: Рабочая директория (по умолчанию директория скрипта)
|
||||
env_vars: Дополнительные переменные окружения
|
||||
|
||||
Returns:
|
||||
QProcess запущенного процесса
|
||||
"""
|
||||
if not os.path.exists(script_path):
|
||||
raise FileNotFoundError(f"Скрипт установки не найден: {script_path}")
|
||||
|
||||
process = QProcess()
|
||||
process.setProcessChannelMode(QProcess.MergedChannels)
|
||||
|
||||
# Настраиваем окружение
|
||||
env = QProcessEnvironment.systemEnvironment()
|
||||
|
||||
# Добавляем пользовательские переменные окружения
|
||||
if env_vars:
|
||||
for key, value in env_vars.items():
|
||||
env.insert(key, value)
|
||||
|
||||
process.setProcessEnvironment(env)
|
||||
|
||||
# Устанавливаем рабочую директорию
|
||||
if work_path:
|
||||
process.setWorkingDirectory(work_path)
|
||||
|
||||
# Запускаем скрипт
|
||||
process.start(script_path)
|
||||
|
||||
if not process.waitForStarted():
|
||||
raise RuntimeError(f"Не удалось запустить процесс установки: {script_path}")
|
||||
|
||||
return process
|
||||
|
||||
def start_manual_install(self, script_path: str,
|
||||
installer_file: str,
|
||||
env_vars: Optional[Dict[str, str]] = None) -> QProcess:
|
||||
"""
|
||||
Запускает ручную установку с указанием файла установщика.
|
||||
|
||||
Args:
|
||||
script_path: Путь к скрипту установки
|
||||
installer_file: Путь к файлу установщика (.exe или .msi)
|
||||
env_vars: Дополнительные переменные окружения
|
||||
|
||||
Returns:
|
||||
QProcess запущенного процесса
|
||||
"""
|
||||
# Для ручной установки передаём путь к файлу через окружение
|
||||
custom_env = env_vars.copy() if env_vars else {}
|
||||
custom_env['USER_INSTALL_FILE'] = installer_file
|
||||
|
||||
return self.start_install(script_path, env_vars=custom_env)
|
||||
|
||||
def stop_install(self, process: QProcess) -> None:
|
||||
"""
|
||||
Останавливает процесс установки.
|
||||
|
||||
|
||||
Args:
|
||||
process: Процесс установки для остановки
|
||||
"""
|
||||
ProcessUtils.stop_process(process)
|
||||
|
||||
def is_running(self, process: QProcess) -> bool:
|
||||
"""
|
||||
Проверяет, запущен ли процесс установки.
|
||||
|
||||
Args:
|
||||
process: Процесс для проверки
|
||||
|
||||
Returns:
|
||||
True если процесс запущен
|
||||
"""
|
||||
return process and process.state() == QProcess.Running
|
||||
|
||||
def connect_output_callback(self, process: QProcess,
|
||||
callback: Callable[[str], None]) -> None:
|
||||
"""
|
||||
Подключает callback для обработки вывода процесса.
|
||||
|
||||
Args:
|
||||
process: Процесс установки
|
||||
callback: Функция для обработки вывода (принимает строку)
|
||||
"""
|
||||
def read_output():
|
||||
output = process.readAllStandardOutput().data().decode('utf-8', 'ignore')
|
||||
if output:
|
||||
callback(output)
|
||||
|
||||
process.readyReadStandardOutput.connect(read_output)
|
||||
|
||||
def connect_error_callback(self, process: QProcess,
|
||||
callback: Callable[[str], None]) -> None:
|
||||
"""
|
||||
Подключает callback для обработки ошибок.
|
||||
|
||||
Args:
|
||||
process: Процесс установки
|
||||
callback: Функция для обработки ошибок (принимает строку)
|
||||
"""
|
||||
def read_error():
|
||||
error = process.readAllStandardError().data().decode('utf-8', 'ignore')
|
||||
if error:
|
||||
callback(error)
|
||||
|
||||
process.readyReadStandardError.connect(read_error)
|
||||
|
||||
def get_install_script_for_folder(self, folder_path: str) -> Optional[str]:
|
||||
"""
|
||||
Возвращает путь к скрипту установки для папки.
|
||||
|
||||
Args:
|
||||
folder_path: Путь к папке скрипта
|
||||
|
||||
Returns:
|
||||
Путь к install.sh или None
|
||||
"""
|
||||
script_path = os.path.join(folder_path, 'install.sh')
|
||||
if os.path.exists(script_path):
|
||||
return script_path
|
||||
return None
|
||||
|
||||
def get_active_installations(self) -> Dict[str, QProcess]:
|
||||
"""
|
||||
Возвращает список активных процессов установки.
|
||||
|
||||
Returns:
|
||||
Словарь {script_path: QProcess}
|
||||
"""
|
||||
return {
|
||||
path: proc for path, proc in self._active_processes.items()
|
||||
if self.is_running(proc)
|
||||
}
|
||||
|
||||
def cancel_all(self) -> None:
|
||||
"""Отменяет все активные процессы установки."""
|
||||
for path, process in list(self._active_processes.items()):
|
||||
if self.is_running(process):
|
||||
self.stop_install(process)
|
||||
self._active_processes.clear()
|
||||
@@ -0,0 +1,270 @@
|
||||
"""
|
||||
Сервис для управления префиксами Wine.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import shutil
|
||||
|
||||
|
||||
class PrefixManager:
|
||||
"""
|
||||
Сервис для управления префиксами Wine.
|
||||
|
||||
Предоставляет функциональность:
|
||||
- Создание префиксов
|
||||
- Удаление префиксов
|
||||
- Получение информации о префиксах
|
||||
- Управление конфигурацией префикса
|
||||
"""
|
||||
|
||||
def __init__(self, user_work_path):
|
||||
"""
|
||||
Инициализирует менеджер префиксов.
|
||||
|
||||
Args:
|
||||
user_work_path: Путь к рабочей директории пользователя
|
||||
"""
|
||||
self.user_work_path = user_work_path
|
||||
self.prefixes_path = os.path.join(user_work_path, "prefixes")
|
||||
|
||||
def get_prefix_path(self, prefix_name):
|
||||
"""
|
||||
Возвращает полный путь к префиксу.
|
||||
|
||||
Args:
|
||||
prefix_name: Имя префикса
|
||||
|
||||
Returns:
|
||||
Полный путь к директории префикса
|
||||
"""
|
||||
return os.path.join(self.prefixes_path, prefix_name)
|
||||
|
||||
def get_prefix_config_path(self, prefix_name, config_file="last.conf"):
|
||||
"""
|
||||
Возвращает путь к файлу конфигурации префикса.
|
||||
|
||||
Args:
|
||||
prefix_name: Имя префикса
|
||||
config_file: Имя файла конфигурации
|
||||
|
||||
Returns:
|
||||
Путь к файлу конфигурации
|
||||
"""
|
||||
prefix_path = self.get_prefix_path(prefix_name)
|
||||
return os.path.join(prefix_path, config_file)
|
||||
|
||||
def prefix_exists(self, prefix_name):
|
||||
"""
|
||||
Проверяет существование префикса.
|
||||
|
||||
Args:
|
||||
prefix_name: Имя префикса
|
||||
|
||||
Returns:
|
||||
True если префикс существует, False иначе
|
||||
"""
|
||||
return os.path.isdir(self.get_prefix_path(prefix_name))
|
||||
|
||||
def list_prefixes(self):
|
||||
"""
|
||||
Возвращает список всех префиксов.
|
||||
|
||||
Returns:
|
||||
Список имен префиксов
|
||||
"""
|
||||
if not os.path.isdir(self.prefixes_path):
|
||||
return []
|
||||
|
||||
try:
|
||||
return sorted([
|
||||
name for name in os.listdir(self.prefixes_path)
|
||||
if os.path.isdir(os.path.join(self.prefixes_path, name))
|
||||
])
|
||||
except OSError:
|
||||
return []
|
||||
|
||||
def create_prefix(self, prefix_name, wine_arch="win64", base_pfx="none"):
|
||||
"""
|
||||
Создает новый префикс.
|
||||
|
||||
Args:
|
||||
prefix_name: Имя префикса
|
||||
wine_arch: Архитектура ("win32" или "win64")
|
||||
base_pfx: Базовый шаблон префикса
|
||||
|
||||
Returns:
|
||||
True если создание успешно, False иначе
|
||||
"""
|
||||
if not prefix_name:
|
||||
return False, "Имя префикса не может быть пустым"
|
||||
|
||||
if not re.match(r'^[a-zA-Z0-9_-]+$', prefix_name):
|
||||
return False, "Имя префикса может содержать только латинские буквы, цифры, дефисы и знаки подчеркивания"
|
||||
|
||||
prefix_path = self.get_prefix_path(prefix_name)
|
||||
if os.path.exists(prefix_path):
|
||||
return False, f"Префикс с именем '{prefix_name}' уже существует"
|
||||
|
||||
# Создаем директорию префикса
|
||||
try:
|
||||
os.makedirs(prefix_path, exist_ok=True)
|
||||
return True, "Префикс создан"
|
||||
except OSError as e:
|
||||
return False, f"Ошибка создания префикса: {e}"
|
||||
|
||||
def delete_prefix(self, prefix_name):
|
||||
"""
|
||||
Удаляет префикс.
|
||||
|
||||
Args:
|
||||
prefix_name: Имя префикса
|
||||
|
||||
Returns:
|
||||
True если удаление успешно, False иначе
|
||||
"""
|
||||
prefix_path = self.get_prefix_path(prefix_name)
|
||||
|
||||
if not os.path.exists(prefix_path):
|
||||
return False, f"Префикс '{prefix_name}' не найден"
|
||||
|
||||
try:
|
||||
shutil.rmtree(prefix_path)
|
||||
return True, "Префикс удален"
|
||||
except OSError as e:
|
||||
return False, f"Ошибка удаления префикса: {e}"
|
||||
|
||||
def read_prefix_config(self, prefix_name, config_file="last.conf"):
|
||||
"""
|
||||
Читает конфигурацию префикса.
|
||||
|
||||
Args:
|
||||
prefix_name: Имя префикса
|
||||
config_file: Имя файла конфигурации
|
||||
|
||||
Returns:
|
||||
Словарь с переменными из конфигурации
|
||||
"""
|
||||
config_path = self.get_prefix_config_path(prefix_name, config_file)
|
||||
|
||||
if not os.path.exists(config_path):
|
||||
return {}
|
||||
|
||||
all_vars = {}
|
||||
try:
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
|
||||
match = re.match(r'export\s+(\w+)="([^"]*)"', line)
|
||||
if match:
|
||||
var_name = match.group(1)
|
||||
var_value = match.group(2)
|
||||
all_vars[var_name] = var_value
|
||||
except (IOError, OSError):
|
||||
return {}
|
||||
|
||||
return all_vars
|
||||
|
||||
def write_prefix_config(self, prefix_name, variables, config_file="last.conf"):
|
||||
"""
|
||||
Записывает конфигурацию префикса.
|
||||
|
||||
Args:
|
||||
prefix_name: Имя префикса
|
||||
variables: Словарь с переменными для записи
|
||||
config_file: Имя файла конфигурации
|
||||
|
||||
Returns:
|
||||
True если запись успешна, False иначе
|
||||
"""
|
||||
config_path = self.get_prefix_config_path(prefix_name, config_file)
|
||||
|
||||
try:
|
||||
# Создаём директорию если не существует
|
||||
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
||||
|
||||
existing_lines = []
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
existing_lines = f.readlines()
|
||||
|
||||
updated_vars = set()
|
||||
new_lines = []
|
||||
|
||||
for line in existing_lines:
|
||||
stripped = line.strip()
|
||||
match = re.match(r'export\s+(\w+)="([^"]*)"', stripped)
|
||||
if match:
|
||||
var_name = match.group(1)
|
||||
if var_name in variables:
|
||||
new_lines.append(f'export {var_name}="{variables[var_name]}"\n')
|
||||
updated_vars.add(var_name)
|
||||
continue
|
||||
new_lines.append(line)
|
||||
|
||||
for var_name in variables:
|
||||
if var_name not in updated_vars:
|
||||
new_lines.append(f'export {var_name}="{variables[var_name]}"\n')
|
||||
|
||||
with open(config_path, 'w', encoding='utf-8') as f:
|
||||
f.writelines(new_lines)
|
||||
|
||||
return True
|
||||
except (IOError, OSError) as e:
|
||||
print(f"Ошибка записи конфигурации: {e}")
|
||||
return False
|
||||
|
||||
def get_prefix_component_version(self, prefix_name, component):
|
||||
"""
|
||||
Возвращает версию установленного компонента в префиксе.
|
||||
|
||||
Args:
|
||||
prefix_name: Имя префикса
|
||||
component: Имя компонента
|
||||
|
||||
Returns:
|
||||
Версия компонента или пустая строка
|
||||
"""
|
||||
config = self.read_prefix_config(prefix_name)
|
||||
return config.get(component, "")
|
||||
|
||||
def open_prefix_folder(self, prefix_name):
|
||||
"""
|
||||
Открывает папку префикса в файловом менеджере.
|
||||
|
||||
Args:
|
||||
prefix_name: Имя префикса
|
||||
|
||||
Returns:
|
||||
True если успешно, False иначе
|
||||
"""
|
||||
prefix_path = self.get_prefix_path(prefix_name)
|
||||
|
||||
if not os.path.isdir(prefix_path):
|
||||
return False, f"Директория префикса не найдена: {prefix_path}"
|
||||
|
||||
try:
|
||||
subprocess.Popen(['xdg-open', prefix_path])
|
||||
return True, "Папка открыта"
|
||||
except Exception as e:
|
||||
return False, f"Ошибка открытия папки: {e}"
|
||||
|
||||
def get_prefixes_with_arch(self):
|
||||
"""
|
||||
Возвращает список префиксов с их архитектурой.
|
||||
|
||||
Returns:
|
||||
Список кортежей (имя, архитектура)
|
||||
"""
|
||||
prefixes = self.list_prefixes()
|
||||
result = []
|
||||
|
||||
for prefix_name in prefixes:
|
||||
config = self.read_prefix_config(prefix_name)
|
||||
arch = config.get('WINE_ARCH', 'unknown')
|
||||
result.append((prefix_name, arch))
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
Сервис для выполнения и обработки процессов.
|
||||
"""
|
||||
import shlex
|
||||
from PyQt5.QtCore import QProcess, QProcessEnvironment
|
||||
|
||||
from winehelperqt.utils.process_utils import ProcessUtils
|
||||
|
||||
|
||||
class ProcessHandler:
|
||||
"""
|
||||
Сервис для выполнения процессов с обработкой вывода.
|
||||
|
||||
Предоставляет функциональность:
|
||||
- Запуск процессов
|
||||
- Обработка вывода (stdout/stderr)
|
||||
- Обработка завершения процесса
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""
|
||||
Инициализирует обработчик процессов.
|
||||
|
||||
Args:
|
||||
parent: Родительский QObject
|
||||
"""
|
||||
self.parent = parent
|
||||
self.process = None
|
||||
self.output_buffer = ""
|
||||
self.log_widget = None
|
||||
|
||||
def set_log_widget(self, log_widget):
|
||||
"""
|
||||
Устанавливает виджет для вывода логов.
|
||||
|
||||
Args:
|
||||
log_widget: QTextEdit для вывода
|
||||
"""
|
||||
self.log_widget = log_widget
|
||||
|
||||
def run_command(self, executable, arguments, log_widget=None):
|
||||
"""
|
||||
Запускает команду и выводит лог.
|
||||
|
||||
Args:
|
||||
executable: Путь к исполняемому файлу
|
||||
arguments: Список аргументов
|
||||
log_widget: QTextEdit для вывода (опционально)
|
||||
"""
|
||||
if log_widget:
|
||||
self.log_widget = log_widget
|
||||
|
||||
self.process = QProcess(self.parent)
|
||||
self.process.setProcessChannelMode(QProcess.MergedChannels)
|
||||
self.process.setProcessEnvironment(QProcessEnvironment.systemEnvironment())
|
||||
|
||||
# Логирование команды
|
||||
if self.log_widget:
|
||||
cmd_line = f"Выполнение: {shlex.quote(executable)} {' '.join(shlex.quote(a) for a in arguments)}"
|
||||
self._append_to_log(cmd_line)
|
||||
|
||||
self.process.start(executable, arguments)
|
||||
return self.process
|
||||
|
||||
def run_simple_command(self, executable, arguments, log_widget=None):
|
||||
"""
|
||||
Запускает простую команду без сложной обработки.
|
||||
|
||||
Args:
|
||||
executable: Путь к исполняемому файлу
|
||||
arguments: Список аргументов
|
||||
log_widget: QTextEdit для вывода (опционально)
|
||||
"""
|
||||
return self.run_command(executable, arguments, log_widget)
|
||||
|
||||
def _append_to_log(self, text, is_error=False):
|
||||
"""Добавляет текст в лог."""
|
||||
if not self.log_widget:
|
||||
return
|
||||
|
||||
if is_error:
|
||||
self.log_widget.append(f'<span style="color:red;">{text}</span>')
|
||||
else:
|
||||
self.log_widget.append(text)
|
||||
|
||||
def get_process(self):
|
||||
"""Возвращает текущий процесс."""
|
||||
return self.process
|
||||
|
||||
def is_running(self):
|
||||
"""Проверяет, запущен ли процесс."""
|
||||
return self.process and self.process.state() == QProcess.Running
|
||||
|
||||
def terminate(self):
|
||||
"""Завершает процесс."""
|
||||
ProcessUtils.stop_process(self.process)
|
||||
|
||||
def kill(self):
|
||||
"""Убивает процесс."""
|
||||
ProcessUtils.stop_process(self.process)
|
||||
|
||||
|
||||
class CommandRunner:
|
||||
"""
|
||||
Утилита для запуска команд winehelper.
|
||||
"""
|
||||
|
||||
def __init__(self, winehelper_path):
|
||||
"""
|
||||
Инициализирует исполнитель команд.
|
||||
|
||||
Args:
|
||||
winehelper_path: Путь к скрипту winehelper
|
||||
"""
|
||||
self.winehelper_path = winehelper_path
|
||||
|
||||
def run(self, command, arguments, log_widget=None):
|
||||
"""
|
||||
Запускает команду winehelper.
|
||||
|
||||
Args:
|
||||
command: Имя команды
|
||||
arguments: Дополнительные аргументы
|
||||
log_widget: QTextEdit для вывода (опционально)
|
||||
"""
|
||||
process_handler = ProcessHandler()
|
||||
process_handler.set_log_widget(log_widget)
|
||||
|
||||
args = [command] + arguments
|
||||
return process_handler.run_command(self.winehelper_path, args, log_widget)
|
||||
|
||||
def remove_prefix(self, prefix_name, log_widget=None):
|
||||
"""
|
||||
Удаляет префикс.
|
||||
|
||||
Args:
|
||||
prefix_name: Имя префикса
|
||||
log_widget: QTextEdit для вывода (опционально)
|
||||
"""
|
||||
return self.run("remove-prefix", [prefix_name, "--force"], log_widget)
|
||||
|
||||
def create_base_prefix(self, prefix_name, log_widget=None):
|
||||
"""
|
||||
Создает базовый префикс.
|
||||
|
||||
Args:
|
||||
prefix_name: Имя префикса
|
||||
log_widget: QTextEdit для вывода (опционально)
|
||||
"""
|
||||
return self.run("create-base-pfx", [prefix_name], log_widget)
|
||||
|
||||
def clear_winetricks_cache(self, log_widget=None):
|
||||
"""
|
||||
Очищает кеш winetricks.
|
||||
|
||||
Args:
|
||||
log_widget: QTextEdit для вывода (опционально)
|
||||
"""
|
||||
return self.run("clear-winetricks-cache", ["--force"], log_widget)
|
||||
@@ -0,0 +1,232 @@
|
||||
"""
|
||||
Сервис для управления скриптами установки WineHelper.
|
||||
|
||||
Предоставляет функциональность:
|
||||
- Загрузка и фильтрация скриптов установки
|
||||
- Получение информации о скриптах (имя, описание, иконки, URL)
|
||||
- Поиск по скриптам
|
||||
- Управление тестовыми скриптами
|
||||
- Кэширование информации о скриптах
|
||||
"""
|
||||
import os
|
||||
from typing import List, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
from winehelperqt.parsers.script_parser import ScriptParser
|
||||
from winehelperqt.utils.cache_service import CacheService
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScriptInfo:
|
||||
"""Информация о скрипте установки."""
|
||||
name: str # Имя программы
|
||||
folder: str # Путь к папке скрипта
|
||||
script_path: str # Путь к install.sh
|
||||
description: str # Описание на русском
|
||||
icons: List[str] # Список путей к иконкам
|
||||
url: str # URL для скачивания (если есть)
|
||||
|
||||
def icon_paths(self) -> List[str]:
|
||||
"""Возвращает полные пути к иконкам."""
|
||||
return self.icons
|
||||
|
||||
|
||||
class ScriptManager:
|
||||
"""Сервис для управления скриптами установки."""
|
||||
|
||||
# Константы для папок со скриптами
|
||||
AUTOINSTALL_DIR = "autoinstall"
|
||||
MANUALINSTALL_DIR = "manualinstall"
|
||||
TESTINSTALL_DIR = "testinstall"
|
||||
|
||||
# TTL для кэширования информации о скриптах (1 час)
|
||||
SCRIPT_INFO_CACHE_TTL = 3600
|
||||
|
||||
def __init__(self, data_path: str, cache_service: Optional[CacheService] = None):
|
||||
"""
|
||||
Инициализирует менеджер скриптов.
|
||||
|
||||
Args:
|
||||
data_path: Путь к директории данных WineHelper
|
||||
cache_service: Сервис кэширования (опционально)
|
||||
"""
|
||||
self.data_path = data_path
|
||||
self.cache_service = cache_service or CacheService(ttl=self.SCRIPT_INFO_CACHE_TTL)
|
||||
self.parser = ScriptParser()
|
||||
|
||||
def get_script_folders(self, folder_name: str) -> List[str]:
|
||||
"""
|
||||
Возвращает список папок со скриптами.
|
||||
|
||||
Args:
|
||||
folder_name: Имя папки (autoinstall, manualinstall, testinstall)
|
||||
|
||||
Returns:
|
||||
Список путей к папкам со скриптами
|
||||
"""
|
||||
folder_path = os.path.join(self.data_path, folder_name)
|
||||
|
||||
if not os.path.isdir(folder_path):
|
||||
return []
|
||||
|
||||
try:
|
||||
return sorted([
|
||||
os.path.join(folder_path, d) for d in os.listdir(folder_path)
|
||||
if os.path.isdir(os.path.join(folder_path, d))
|
||||
])
|
||||
except OSError:
|
||||
return []
|
||||
|
||||
def get_script_info(self, script_path: str) -> Optional[ScriptInfo]:
|
||||
"""
|
||||
Получает информацию о скрипте установки.
|
||||
|
||||
Args:
|
||||
script_path: Путь к скрипту (install.sh)
|
||||
|
||||
Returns:
|
||||
ScriptInfo или None если информация не найдена
|
||||
"""
|
||||
# Создаём ключ кэша на основе пути и времени модификации файла
|
||||
cache_key = self._get_script_cache_key(script_path)
|
||||
|
||||
# Проверяем кэш
|
||||
cached = self.cache_service.get(cache_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
# Проверяем существование файла
|
||||
if not os.path.exists(script_path):
|
||||
return None
|
||||
|
||||
# Парсим информацию из скрипта
|
||||
prog_name = self.parser.extract_prog_name_from_script(script_path)
|
||||
description = self.parser.extract_info_ru(script_path)
|
||||
icons = self.parser.extract_icons_from_script(script_path)
|
||||
url = self.parser.extract_prog_url_from_script(script_path)
|
||||
|
||||
# Получаем папку скрипта
|
||||
script_folder = os.path.dirname(script_path)
|
||||
|
||||
# Если имя программы не найдено, используем имя папки как fallback
|
||||
if not prog_name:
|
||||
prog_name = os.path.basename(script_folder)
|
||||
|
||||
script_info = ScriptInfo(
|
||||
name=prog_name,
|
||||
folder=script_folder,
|
||||
script_path=script_path,
|
||||
description=description or "",
|
||||
icons=icons or [],
|
||||
url=url or ""
|
||||
)
|
||||
|
||||
# Сохраняем в кэш
|
||||
self.cache_service.set(cache_key, script_info)
|
||||
|
||||
return script_info
|
||||
|
||||
def list_available_apps(self, folder_name: str) -> List[ScriptInfo]:
|
||||
"""
|
||||
Возвращает список доступных приложений из папки.
|
||||
|
||||
Args:
|
||||
folder_name: Имя папки со скриптами
|
||||
|
||||
Returns:
|
||||
Список ScriptInfo
|
||||
"""
|
||||
folders = self.get_script_folders(folder_name)
|
||||
apps = []
|
||||
|
||||
for folder in folders:
|
||||
script_file = os.path.join(folder, 'install.sh')
|
||||
if os.path.exists(script_file):
|
||||
info = self.get_script_info(script_file)
|
||||
if info:
|
||||
apps.append(info)
|
||||
|
||||
return apps
|
||||
|
||||
def get_autoinstall_scripts(self) -> List[ScriptInfo]:
|
||||
"""Возвращает список скриптов для автоматической установки."""
|
||||
return self.list_available_apps(self.AUTOINSTALL_DIR)
|
||||
|
||||
def get_manualinstall_scripts(self) -> List[ScriptInfo]:
|
||||
"""Возвращает список скриптов для ручной установки."""
|
||||
return self.list_available_apps(self.MANUALINSTALL_DIR)
|
||||
|
||||
def get_testinstall_scripts(self) -> List[ScriptInfo]:
|
||||
"""Возвращает список скриптов для тестовой установки."""
|
||||
return self.list_available_apps(self.TESTINSTALL_DIR)
|
||||
|
||||
def search_scripts(self, scripts: List[ScriptInfo], search_text: str) -> List[ScriptInfo]:
|
||||
"""
|
||||
Ищет скрипты по названию и описанию.
|
||||
|
||||
Args:
|
||||
scripts: Список скриптов для поиска
|
||||
search_text: Текст для поиска
|
||||
|
||||
Returns:
|
||||
Отфильтрованный список скриптов
|
||||
"""
|
||||
if not search_text or not search_text.strip():
|
||||
return scripts
|
||||
|
||||
search_lower = search_text.lower()
|
||||
return [
|
||||
script for script in scripts
|
||||
if search_lower in script.name.lower() or
|
||||
search_lower in script.description.lower()
|
||||
]
|
||||
|
||||
def get_script_by_name(self, name: str, folder_name: str) -> Optional[ScriptInfo]:
|
||||
"""
|
||||
Находит скрипт по имени в указанной папке.
|
||||
|
||||
Args:
|
||||
name: Имя скрипта
|
||||
folder_name: Имя папки
|
||||
|
||||
Returns:
|
||||
ScriptInfo или None
|
||||
"""
|
||||
scripts = self.list_available_apps(folder_name)
|
||||
for script in scripts:
|
||||
if script.name.lower() == name.lower():
|
||||
return script
|
||||
return None
|
||||
|
||||
def clear_cache(self) -> None:
|
||||
"""Очищает кэш информации о скриптах."""
|
||||
self.cache_service.clear()
|
||||
|
||||
def invalidate_script_cache(self, script_path: str) -> bool:
|
||||
"""
|
||||
Удаляет конкретный скрипт из кэша.
|
||||
|
||||
Args:
|
||||
script_path: Путь к скрипту
|
||||
|
||||
Returns:
|
||||
True если запись была в кэше
|
||||
"""
|
||||
cache_key = self._get_script_cache_key(script_path)
|
||||
return self.cache_service.delete(cache_key)
|
||||
|
||||
def _get_script_cache_key(self, script_path: str) -> str:
|
||||
"""
|
||||
Создаёт ключ кэша для скрипта на основе пути и времени модификации.
|
||||
|
||||
Args:
|
||||
script_path: Путь к скрипту
|
||||
|
||||
Returns:
|
||||
Ключ для кэша
|
||||
"""
|
||||
try:
|
||||
mtime = os.path.getmtime(script_path)
|
||||
return f"script_info:{script_path}:{mtime}"
|
||||
except OSError:
|
||||
return f"script_info:{script_path}"
|
||||
@@ -0,0 +1,228 @@
|
||||
"""
|
||||
Сервис для управления настройками WineHelper.
|
||||
|
||||
Предоставляет функциональность:
|
||||
- Чтение/запись настроек префикса
|
||||
- Дополнительные настройки приложения
|
||||
- Backup/restore настроек
|
||||
- Загрузка настроек по умолчанию
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
from typing import Dict, Optional
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class SettingsManager:
|
||||
"""
|
||||
Сервис для управления настройками приложений и префиксов.
|
||||
|
||||
Примерты использования:
|
||||
manager = SettingsManager(prefix_manager)
|
||||
settings = manager.load_prefix_settings('my_prefix')
|
||||
settings['WINE_ARCH'] = 'win32'
|
||||
manager.save_prefix_settings('my_prefix', settings)
|
||||
"""
|
||||
|
||||
def __init__(self, prefix_manager):
|
||||
"""
|
||||
Инициализирует менеджер настроек.
|
||||
|
||||
Args:
|
||||
prefix_manager: Экземпляр PrefixManager
|
||||
"""
|
||||
self.prefix_manager = prefix_manager
|
||||
|
||||
def load_prefix_settings(self, prefix_name: str) -> Dict[str, str]:
|
||||
"""
|
||||
Загружает настройки префикса из last.conf.
|
||||
|
||||
Args:
|
||||
prefix_name: Имя префикса
|
||||
|
||||
Returns:
|
||||
Словарь с настройками
|
||||
"""
|
||||
return self.prefix_manager.read_prefix_config(prefix_name)
|
||||
|
||||
def save_prefix_settings(self, prefix_name: str, settings: Dict[str, str]) -> bool:
|
||||
"""
|
||||
Сохраняет настройки префикса в last.conf.
|
||||
|
||||
Args:
|
||||
prefix_name: Имя префикса
|
||||
settings: Словарь с настройками
|
||||
|
||||
Returns:
|
||||
True если сохранение успешно
|
||||
"""
|
||||
return self.prefix_manager.write_prefix_config(prefix_name, settings)
|
||||
|
||||
def load_app_settings(self, desktop_path: str) -> Dict[str, str]:
|
||||
"""
|
||||
Загружает дополнительные настройки приложения.
|
||||
|
||||
Args:
|
||||
desktop_path: Путь к .desktop файлу
|
||||
|
||||
Returns:
|
||||
Словарь с настройками приложения
|
||||
"""
|
||||
if not os.path.exists(desktop_path):
|
||||
return {}
|
||||
|
||||
settings = {}
|
||||
try:
|
||||
with open(desktop_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if '=' in line and not line.startswith('#'):
|
||||
key, value = line.split('=', 1)
|
||||
settings[key.strip()] = value.strip()
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
|
||||
return settings
|
||||
|
||||
def save_app_settings(self, desktop_path: str, settings: Dict[str, str]) -> bool:
|
||||
"""
|
||||
Сохраняет дополнительные настройки приложения.
|
||||
|
||||
Args:
|
||||
desktop_path: Путь к .desktop файлу
|
||||
settings: Словарь с настройками
|
||||
|
||||
Returns:
|
||||
True если сохранение успешно
|
||||
"""
|
||||
if not os.path.exists(desktop_path):
|
||||
return False
|
||||
|
||||
try:
|
||||
# Читаем существующие настройки
|
||||
existing_settings = {}
|
||||
with open(desktop_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if '=' in line and not line.startswith('#'):
|
||||
key, value = line.split('=', 1)
|
||||
existing_settings[key.strip()] = value.strip()
|
||||
|
||||
# Объединяем с новыми настройками
|
||||
existing_settings.update(settings)
|
||||
|
||||
# Перезаписываем файл
|
||||
with open(desktop_path, 'w', encoding='utf-8') as f:
|
||||
f.write("[Desktop Entry]\n")
|
||||
for key, value in existing_settings.items():
|
||||
f.write(f"{key}={value}\n")
|
||||
|
||||
return True
|
||||
|
||||
except (IOError, OSError) as e:
|
||||
print(f"Ошибка сохранения настроек: {e}")
|
||||
return False
|
||||
|
||||
def backup_settings(self, source_path: str, backup_dir: Optional[str] = None) -> Optional[str]:
|
||||
"""
|
||||
Создаёт резервную копию файла настроек.
|
||||
|
||||
Args:
|
||||
source_path: Путь к файлу настроек
|
||||
backup_dir: Директория для бэкапа (по умолчанию - рядом с файлом)
|
||||
|
||||
Returns:
|
||||
Путь к файлу бэкапа или None
|
||||
"""
|
||||
if not os.path.exists(source_path):
|
||||
return None
|
||||
|
||||
try:
|
||||
# Определяем директорию для бэкапа
|
||||
if backup_dir is None:
|
||||
backup_dir = os.path.join(
|
||||
os.path.dirname(source_path),
|
||||
"backups"
|
||||
)
|
||||
|
||||
os.makedirs(backup_dir, exist_ok=True)
|
||||
|
||||
# Формируем имя файла бэкапа
|
||||
filename = os.path.basename(source_path)
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
backup_filename = f"{filename}.{timestamp}.backup"
|
||||
backup_path = os.path.join(backup_dir, backup_filename)
|
||||
|
||||
# Копируем файл
|
||||
shutil.copy2(source_path, backup_path)
|
||||
return backup_path
|
||||
|
||||
except (IOError, OSError) as e:
|
||||
print(f"Ошибка создания бэкапа: {e}")
|
||||
return None
|
||||
|
||||
def restore_settings(self, backup_path: str, target_path: str) -> bool:
|
||||
"""
|
||||
Восстанавливает настройки из бэкапа.
|
||||
|
||||
Args:
|
||||
backup_path: Путь к файлу бэкапа
|
||||
target_path: Путь для восстановления
|
||||
|
||||
Returns:
|
||||
True если восстановление успешно
|
||||
"""
|
||||
if not os.path.exists(backup_path):
|
||||
return False
|
||||
|
||||
try:
|
||||
# Создаём директорию назначения
|
||||
target_dir = os.path.dirname(target_path)
|
||||
if target_dir:
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
|
||||
# Копируем файл бэкапа
|
||||
shutil.copy2(backup_path, target_path)
|
||||
return True
|
||||
|
||||
except (IOError, OSError) as e:
|
||||
print(f"Ошибка восстановления настроек: {e}")
|
||||
return False
|
||||
|
||||
def get_default_settings(self, prefix_name: str) -> Dict[str, str]:
|
||||
"""
|
||||
Возвращает настройки по умолчанию для префикса.
|
||||
|
||||
Args:
|
||||
prefix_name: Имя префикса
|
||||
|
||||
Returns:
|
||||
Словарь с настройками по умолчанию
|
||||
"""
|
||||
return {
|
||||
'WINE_ARCH': 'win64',
|
||||
'WINEESYNC': '0',
|
||||
'WINEFSYNC': '0',
|
||||
'WH_WINDOWS_VER': '10',
|
||||
'WH_USE_SHADER_CACHE': '1',
|
||||
'WH_USE_MESA_GL_OVERRIDE': '0',
|
||||
'WH_USE_WINE_DXGI': '0',
|
||||
'WINE_LARGE_ADDRESS_AWARE': '1',
|
||||
'WINE_FULLSCREEN_FSR': '1',
|
||||
}
|
||||
|
||||
def merge_settings(self, base: Dict[str, str],
|
||||
override: Dict[str, str]) -> Dict[str, str]:
|
||||
"""
|
||||
Объединяет две конфигурации настроек.
|
||||
|
||||
Args:
|
||||
base: Базовые настройки
|
||||
override: Переопределяющие настройки
|
||||
|
||||
Returns:
|
||||
Объединённый словарь
|
||||
"""
|
||||
result = base.copy()
|
||||
result.update(override)
|
||||
return result
|
||||
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
Сервисы для управления кэшем и компонентами Winetricks.
|
||||
"""
|
||||
import os
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
from winehelperqt.parsers.winetricks_parser import WinetricksOutputParser
|
||||
from winehelperqt.utils.cache_service import FileCacheService
|
||||
|
||||
|
||||
class WinetricksCacheService:
|
||||
"""
|
||||
Сервис для управления кэшем Winetricks.
|
||||
|
||||
Использует FileCacheService для хранения + проверку хеша winetricks.
|
||||
"""
|
||||
|
||||
DEFAULT_CACHE_TTL_SECONDS = 86400
|
||||
CACHE_DIR = Path.home() / ".cache" / "winehelper" / "winetricks"
|
||||
|
||||
def __init__(self, cache_dir=None, cache_ttl=None):
|
||||
"""
|
||||
Инициализирует сервис кэша.
|
||||
|
||||
Args:
|
||||
cache_dir: Путь к директории кэша
|
||||
cache_ttl: Время жизни кэша в секундах
|
||||
"""
|
||||
self.cache_dir = Path(cache_dir) if cache_dir else self.CACHE_DIR
|
||||
self.cache_ttl = cache_ttl if cache_ttl is not None else self.DEFAULT_CACHE_TTL_SECONDS
|
||||
self._ensure_cache_dir()
|
||||
self._file_cache = FileCacheService(cache_dir=str(self.cache_dir), cache_ttl=self.cache_ttl)
|
||||
|
||||
def _ensure_cache_dir(self):
|
||||
"""Создает директорию кэша если она не существует."""
|
||||
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _get_winetricks_hash(self, winetricks_path):
|
||||
"""
|
||||
Вычисляет SHA256 хэш файла winetricks.
|
||||
|
||||
Args:
|
||||
winetricks_path: Путь к файлу winetricks
|
||||
"""
|
||||
try:
|
||||
hasher = hashlib.sha256()
|
||||
with open(winetricks_path, 'rb') as f:
|
||||
while chunk := f.read(4096):
|
||||
hasher.update(chunk)
|
||||
return hasher.hexdigest()
|
||||
except (IOError, OSError):
|
||||
return None
|
||||
|
||||
def get_cached_data(self, category, winetricks_path):
|
||||
"""
|
||||
Получает данные из кэша если они актуальны.
|
||||
|
||||
Args:
|
||||
category: Категория компонентов
|
||||
winetricks_path: Путь к файлу winetricks
|
||||
"""
|
||||
cached = self._file_cache.get_json(category)
|
||||
if cached is None:
|
||||
return None
|
||||
|
||||
winetricks_hash = self._get_winetricks_hash(winetricks_path)
|
||||
if cached.get("hash") != winetricks_hash:
|
||||
return None
|
||||
|
||||
return cached.get("output")
|
||||
|
||||
def save_to_cache(self, category, output, winetricks_path):
|
||||
"""
|
||||
Сохраняет данные в кэш.
|
||||
|
||||
Args:
|
||||
category: Категория компонентов
|
||||
output: Данные для сохранения
|
||||
winetricks_path: Путь к файлу winetricks
|
||||
"""
|
||||
winetricks_hash = self._get_winetricks_hash(winetricks_path)
|
||||
|
||||
if not winetricks_hash:
|
||||
return False
|
||||
|
||||
cache_data = {
|
||||
"hash": winetricks_hash,
|
||||
"output": output
|
||||
}
|
||||
|
||||
return self._file_cache.set_json(category, cache_data)
|
||||
|
||||
def clear_cache(self):
|
||||
"""
|
||||
Очищает весь кэш Winetricks.
|
||||
"""
|
||||
return self._file_cache.clear()
|
||||
|
||||
def is_cache_valid(self, category, winetricks_path):
|
||||
"""
|
||||
Проверяет актуальность кэша для категории.
|
||||
"""
|
||||
return self.get_cached_data(category, winetricks_path) is not None
|
||||
|
||||
def get_cache_age(self, category):
|
||||
"""
|
||||
Возвращает возраст кэша в секундах.
|
||||
"""
|
||||
return self._file_cache.get_cache_age(category)
|
||||
|
||||
|
||||
class WinetricksLogReader:
|
||||
"""
|
||||
Сервис для чтения и анализа лога Winetricks.
|
||||
"""
|
||||
|
||||
def __init__(self, prefix_path):
|
||||
"""
|
||||
Инициализирует читатель лога.
|
||||
|
||||
Args:
|
||||
prefix_path: Путь к префиксу Wine
|
||||
"""
|
||||
self.prefix_path = prefix_path
|
||||
self.log_path = os.path.join(prefix_path, "winetricks.log")
|
||||
|
||||
def get_installed_verbs(self):
|
||||
"""
|
||||
Читает winetricks.log и возвращает множество установленных компонентов.
|
||||
"""
|
||||
return WinetricksOutputParser.parse_installed_verbs(self.log_path)
|
||||
|
||||
def is_verb_installed(self, verb):
|
||||
"""
|
||||
Проверяет, установлен ли указанный компонент.
|
||||
"""
|
||||
installed_verbs = self.get_installed_verbs()
|
||||
return verb in installed_verbs
|
||||
|
||||
def get_log_path(self):
|
||||
"""Возвращает путь к файлу лога."""
|
||||
return self.log_path
|
||||
|
||||
def log_exists(self):
|
||||
"""Проверяет существование файла лога."""
|
||||
return os.path.exists(self.log_path)
|
||||
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
Утилиты для winehelper GUI.
|
||||
"""
|
||||
from winehelperqt.utils.log_handler import LogProgressHandler, MultiLogHandler
|
||||
from winehelperqt.utils.cache_service import CacheService, FileCacheService
|
||||
|
||||
__all__ = [
|
||||
'LogProgressHandler',
|
||||
'MultiLogHandler',
|
||||
'CacheService',
|
||||
'FileCacheService',
|
||||
]
|
||||
@@ -0,0 +1,334 @@
|
||||
"""
|
||||
Универсальный сервис кэширования для WineHelper.
|
||||
|
||||
Предоставляет функциональность:
|
||||
- TTL-based кэширование (time-to-live)
|
||||
- In-memory кэш для быстрых операций
|
||||
- File-based кэш для постоянных данных
|
||||
- Автоматическая очистка устаревших записей
|
||||
"""
|
||||
import json
|
||||
import time
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, Dict
|
||||
|
||||
|
||||
class CacheEntry:
|
||||
"""Запись в кэше с метаданными."""
|
||||
|
||||
def __init__(self, data: Any, ttl: Optional[float] = None):
|
||||
"""
|
||||
Инициализирует запись кэша.
|
||||
|
||||
Args:
|
||||
data: Данные для кэширования
|
||||
ttl: Время жизни в секундах (None = бессрочно)
|
||||
"""
|
||||
self.data = data
|
||||
self.timestamp = time.time()
|
||||
self.ttl = ttl
|
||||
|
||||
def is_expired(self) -> bool:
|
||||
"""Проверяет, истекло ли время жизни записи."""
|
||||
if self.ttl is None:
|
||||
return False
|
||||
return (time.time() - self.timestamp) > self.ttl
|
||||
|
||||
def age(self) -> float:
|
||||
"""Возвращает возраст записи в секундах."""
|
||||
return time.time() - self.timestamp
|
||||
|
||||
|
||||
class CacheService:
|
||||
"""Универсальный сервис кэширования с поддержкой TTL."""
|
||||
|
||||
def __init__(self, ttl: Optional[float] = None, max_size: int = 1000):
|
||||
"""
|
||||
Инициализирует сервис кэширования.
|
||||
|
||||
Args:
|
||||
ttl: Время жизни по умолчанию (в секундах)
|
||||
max_size: Максимальный размер кэша в памяти
|
||||
"""
|
||||
self._cache: Dict[str, CacheEntry] = {}
|
||||
self._default_ttl = ttl
|
||||
self._max_size = max_size
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
"""
|
||||
Получает значение из кэша.
|
||||
|
||||
Args:
|
||||
key: Ключ
|
||||
default: Значение по умолчанию, если ключ не найден
|
||||
|
||||
Returns:
|
||||
Значение из кэша или default
|
||||
"""
|
||||
entry = self._cache.get(key)
|
||||
if entry is None:
|
||||
return default
|
||||
|
||||
if entry.is_expired():
|
||||
self._cache.pop(key, None)
|
||||
return default
|
||||
|
||||
return entry.data
|
||||
|
||||
def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None:
|
||||
"""
|
||||
Сохраняет значение в кэш.
|
||||
|
||||
Args:
|
||||
key: Ключ
|
||||
value: Значение
|
||||
ttl: Время жизни (по умолчанию из настроек сервиса)
|
||||
"""
|
||||
# Проверяем размер кэша
|
||||
if len(self._cache) >= self._max_size and key not in self._cache:
|
||||
self._evict_expired()
|
||||
|
||||
# Если всё ещё превышен размер, удаляем oldest entry
|
||||
if len(self._cache) >= self._max_size and key not in self._cache:
|
||||
self._evict_oldest()
|
||||
|
||||
effective_ttl = ttl if ttl is not None else self._default_ttl
|
||||
self._cache[key] = CacheEntry(value, effective_ttl)
|
||||
|
||||
def delete(self, key: str) -> bool:
|
||||
"""
|
||||
Удаляет запись из кэша.
|
||||
|
||||
Args:
|
||||
key: Ключ для удаления
|
||||
|
||||
Returns:
|
||||
True если запись была удалена
|
||||
"""
|
||||
return self._cache.pop(key, None) is not None
|
||||
|
||||
def has(self, key: str) -> bool:
|
||||
"""
|
||||
Проверяет наличие ключа в кэше.
|
||||
|
||||
Args:
|
||||
key: Ключ
|
||||
|
||||
Returns:
|
||||
True если ключ существует и не истек
|
||||
"""
|
||||
entry = self._cache.get(key)
|
||||
if entry is None:
|
||||
return False
|
||||
|
||||
if entry.is_expired():
|
||||
self._cache.pop(key, None)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Очищает весь кэш."""
|
||||
self._cache.clear()
|
||||
|
||||
def size(self) -> int:
|
||||
"""Возвращает количество записей в кэше."""
|
||||
return len(self._cache)
|
||||
|
||||
def _evict_expired(self) -> None:
|
||||
"""Удаляет все истекшие записи."""
|
||||
expired_keys = [
|
||||
key for key, entry in self._cache.items()
|
||||
if entry.is_expired()
|
||||
]
|
||||
for key in expired_keys:
|
||||
del self._cache[key]
|
||||
|
||||
def _evict_oldest(self) -> None:
|
||||
"""Удаляет самую старую запись."""
|
||||
if not self._cache:
|
||||
return
|
||||
|
||||
oldest_key = min(
|
||||
self._cache.keys(),
|
||||
key=lambda k: self._cache[k].timestamp
|
||||
)
|
||||
del self._cache[oldest_key]
|
||||
|
||||
def cleanup(self) -> int:
|
||||
"""
|
||||
Очищает истекшие записи.
|
||||
|
||||
Returns:
|
||||
Количество удалённых записей
|
||||
"""
|
||||
before = len(self._cache)
|
||||
self._evict_expired()
|
||||
return before - len(self._cache)
|
||||
|
||||
|
||||
class FileCacheService:
|
||||
"""Сервис файлового кэширования для постоянных данных."""
|
||||
|
||||
DEFAULT_CACHE_TTL_SECONDS = 86400 # 24 часа
|
||||
|
||||
def __init__(self, cache_dir: Optional[str] = None, cache_ttl: Optional[float] = None):
|
||||
"""
|
||||
Инициализирует сервис файлового кэша.
|
||||
|
||||
Args:
|
||||
cache_dir: Путь к директории кэша
|
||||
cache_ttl: Время жизни кэша в секундах
|
||||
"""
|
||||
self.cache_dir = Path(cache_dir) if cache_dir else self._default_cache_dir()
|
||||
self.cache_ttl = cache_ttl if cache_ttl is not None else self.DEFAULT_CACHE_TTL_SECONDS
|
||||
self._ensure_cache_dir()
|
||||
|
||||
def _default_cache_dir(self) -> Path:
|
||||
"""Возвращает директорию кэша по умолчанию."""
|
||||
return Path.home() / ".cache" / "winehelper"
|
||||
|
||||
def _ensure_cache_dir(self) -> None:
|
||||
"""Создаёт директорию кэша, если она не существует."""
|
||||
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _get_cache_path(self, key: str) -> Path:
|
||||
"""
|
||||
Возвращает путь к файлу кэша.
|
||||
|
||||
Args:
|
||||
key: Ключ (будет преобразован в безопасное имя файла)
|
||||
|
||||
Returns:
|
||||
Путь к файлу кэша
|
||||
"""
|
||||
# Хэшируем ключ для безопасного имени файла
|
||||
key_hash = hashlib.md5(key.encode()).hexdigest()
|
||||
return self.cache_dir / f"{key_hash}.cache"
|
||||
|
||||
def get_json(self, key: str, default: Any = None) -> Any:
|
||||
"""
|
||||
Получает JSON данные из файлового кэша.
|
||||
|
||||
Args:
|
||||
key: Ключ
|
||||
default: Значение по умолчанию
|
||||
|
||||
Returns:
|
||||
Данные из кэша или default
|
||||
"""
|
||||
cache_path = self._get_cache_path(key)
|
||||
|
||||
if not cache_path.exists():
|
||||
return default
|
||||
|
||||
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)
|
||||
if cache_age < self.cache_ttl:
|
||||
return cache_data.get("data")
|
||||
else:
|
||||
# Кэш устарел, удаляем
|
||||
cache_path.unlink(missing_ok=True)
|
||||
return default
|
||||
|
||||
except (json.JSONDecodeError, IOError, KeyError) as e:
|
||||
print(f"Кэш '{key}' повреждён: {e}")
|
||||
return default
|
||||
|
||||
def set_json(self, key: str, data: Any) -> bool:
|
||||
"""
|
||||
Сохраняет данные в файловый кэш как JSON.
|
||||
|
||||
Args:
|
||||
key: Ключ
|
||||
data: Данные для сохранения
|
||||
|
||||
Returns:
|
||||
True если сохранение успешно
|
||||
"""
|
||||
cache_path = self._get_cache_path(key)
|
||||
|
||||
cache_data = {
|
||||
"timestamp": time.time(),
|
||||
"data": data
|
||||
}
|
||||
|
||||
try:
|
||||
with open(cache_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(cache_data, f, ensure_ascii=False, indent=2)
|
||||
return True
|
||||
except (IOError, OSError) as e:
|
||||
print(f"Не удалось сохранить кэш '{key}': {e}")
|
||||
return False
|
||||
|
||||
def delete(self, key: str) -> bool:
|
||||
"""
|
||||
Удаляет запись из файлового кэша.
|
||||
|
||||
Args:
|
||||
key: Ключ для удаления
|
||||
|
||||
Returns:
|
||||
True если запись была удалена
|
||||
"""
|
||||
cache_path = self._get_cache_path(key)
|
||||
try:
|
||||
cache_path.unlink(missing_ok=True)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
def clear(self) -> bool:
|
||||
"""
|
||||
Очищает весь файловый кэш.
|
||||
|
||||
Returns:
|
||||
True если очистка успешна
|
||||
"""
|
||||
try:
|
||||
if self.cache_dir.exists():
|
||||
for file_path in self.cache_dir.glob("*.cache"):
|
||||
file_path.unlink()
|
||||
return True
|
||||
except (IOError, OSError) as e:
|
||||
print(f"Ошибка очистки кэша: {e}")
|
||||
return False
|
||||
|
||||
def get_cache_age(self, key: str) -> Optional[float]:
|
||||
"""
|
||||
Возвращает возраст кэша в секундах.
|
||||
|
||||
Args:
|
||||
key: Ключ
|
||||
|
||||
Returns:
|
||||
Возраст в секундах или None
|
||||
"""
|
||||
cache_path = self._get_cache_path(key)
|
||||
|
||||
if not cache_path.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(cache_path, 'r', encoding='utf-8') as f:
|
||||
cache_data = json.load(f)
|
||||
return time.time() - cache_data.get("timestamp", 0)
|
||||
except (json.JSONDecodeError, IOError, KeyError):
|
||||
return None
|
||||
|
||||
def is_valid(self, key: str) -> bool:
|
||||
"""
|
||||
Проверяет актуальность кэша.
|
||||
|
||||
Args:
|
||||
key: Ключ
|
||||
|
||||
Returns:
|
||||
True если кэш актуален
|
||||
"""
|
||||
return self.get_json(key) is not None
|
||||
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Единый модуль для переменных окружения WineHelper.
|
||||
|
||||
Устраняет дублирование класса Var в нескольких файлах.
|
||||
"""
|
||||
import os
|
||||
|
||||
|
||||
class EnvironmentConfig:
|
||||
"""Полная конфигурация приложения из переменных окружения."""
|
||||
# Переменные определяемые в скрипте 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")
|
||||
WH_ICON_TRAY = os.environ.get("WH_ICON_TRAY")
|
||||
LICENSE_FILE = os.environ.get("LICENSE_FILE")
|
||||
LICENSE_AGREEMENT_FILE = os.environ.get("AGREEMENT")
|
||||
THIRD_PARTY_FILE = os.environ.get("THIRD_PARTY_FILE")
|
||||
GENERAL = os.environ.get("GENERAL")
|
||||
WH_WINETRICKS = os.environ.get("WH_WINETRICKS")
|
||||
WH_DIST_DIR = os.environ.get("WH_DIST_DIR")
|
||||
|
||||
|
||||
# Alias
|
||||
Var = EnvironmentConfig
|
||||
@@ -0,0 +1,230 @@
|
||||
"""
|
||||
Обработчик логов с поддержкой прогресс-баров.
|
||||
Обрабатывает вывод процессов с корректным отображением однострочного прогресса.
|
||||
"""
|
||||
import re
|
||||
from PyQt5.QtGui import QTextCursor
|
||||
|
||||
|
||||
class LogProgressHandler:
|
||||
"""
|
||||
Обработчик логов для процессов с прогресс-барами.
|
||||
|
||||
Корректно обрабатывает строки с '\r' (carriage return),
|
||||
заменяя предыдущие строки прогресса новыми, вместо добавления новых строк.
|
||||
"""
|
||||
|
||||
PROGRESS_PERCENT_PATTERN = re.compile(r'\d\s*%')
|
||||
|
||||
def __init__(self, log_widget=None):
|
||||
"""
|
||||
Инициализирует обработчик логов.
|
||||
|
||||
Args:
|
||||
log_widget: Виджет QTextEdit для вывода логов
|
||||
"""
|
||||
self.log_widget = log_widget
|
||||
self.output_buffer = ""
|
||||
self.last_line_was_progress = False
|
||||
|
||||
def set_log_widget(self, log_widget):
|
||||
"""Устанавливает виджет для вывода логов."""
|
||||
self.log_widget = log_widget
|
||||
|
||||
def process_output(self, raw_output):
|
||||
"""
|
||||
Обрабатывает вывод процесса, разбивая на строки и обрабатывая прогресс.
|
||||
|
||||
Args:
|
||||
raw_output: Сырой вывод от процесса
|
||||
"""
|
||||
if not self.log_widget:
|
||||
return
|
||||
|
||||
self.output_buffer += raw_output
|
||||
|
||||
while True:
|
||||
idx_n = self.output_buffer.find('\n')
|
||||
idx_r = self.output_buffer.find('\r')
|
||||
|
||||
if idx_n == -1 and idx_r == -1:
|
||||
break
|
||||
|
||||
split_idx = min(idx for idx in [idx_n, idx_r] if idx != -1)
|
||||
line = self.output_buffer[:split_idx + 1]
|
||||
self.output_buffer = self.output_buffer[split_idx + 1:]
|
||||
|
||||
self._process_log_line(line)
|
||||
|
||||
def process_line(self, line):
|
||||
"""
|
||||
Обрабатывает одну строку лога.
|
||||
|
||||
Args:
|
||||
line: Строка лога (может содержать '\r' или '\n')
|
||||
"""
|
||||
if not self.log_widget:
|
||||
return
|
||||
|
||||
self._process_log_line(line)
|
||||
|
||||
def finalize(self):
|
||||
"""
|
||||
Завершает обработку лога.
|
||||
Обрабатывает остаток буфера и добавляет перенос строки если нужно.
|
||||
"""
|
||||
if not self.log_widget:
|
||||
return
|
||||
|
||||
if self.output_buffer:
|
||||
self._process_log_line(self.output_buffer)
|
||||
|
||||
if self.last_line_was_progress:
|
||||
cursor = self.log_widget.textCursor()
|
||||
cursor.movePosition(QTextCursor.End)
|
||||
cursor.insertText("\n")
|
||||
|
||||
self._reset_state()
|
||||
|
||||
def _process_log_line(self, line_with_delimiter):
|
||||
"""
|
||||
Обрабатывает одну строку лога, управляя заменой строк прогресса.
|
||||
|
||||
Args:
|
||||
line_with_delimiter: Строка с разделителем (\n или \r)
|
||||
"""
|
||||
is_progress_line = '\r' in line_with_delimiter
|
||||
|
||||
if is_progress_line:
|
||||
if not self.PROGRESS_PERCENT_PATTERN.search(line_with_delimiter):
|
||||
return
|
||||
|
||||
clean_line = line_with_delimiter.replace('\r', '').replace('\n', '').strip()
|
||||
|
||||
if not clean_line:
|
||||
return
|
||||
|
||||
cursor = self.log_widget.textCursor()
|
||||
|
||||
if is_progress_line and self.last_line_was_progress:
|
||||
cursor.movePosition(QTextCursor.End)
|
||||
cursor.select(QTextCursor.LineUnderCursor)
|
||||
cursor.removeSelectedText()
|
||||
elif not is_progress_line and self.last_line_was_progress:
|
||||
cursor.movePosition(QTextCursor.End)
|
||||
cursor.select(QTextCursor.LineUnderCursor)
|
||||
cursor.removeSelectedText()
|
||||
|
||||
add_newline = not is_progress_line
|
||||
self._append_to_log(clean_line, is_error=False, add_newline=add_newline)
|
||||
|
||||
self.last_line_was_progress = is_progress_line
|
||||
|
||||
def _append_to_log(self, text, is_error=False, add_newline=True):
|
||||
"""
|
||||
Добавляет сообщение в лог.
|
||||
|
||||
Args:
|
||||
text: Текст сообщения
|
||||
is_error: True если это ошибка (красный цвет)
|
||||
add_newline: True если добавить перенос строки
|
||||
"""
|
||||
if not self.log_widget:
|
||||
return
|
||||
|
||||
cursor = self.log_widget.textCursor()
|
||||
cursor.movePosition(QTextCursor.End)
|
||||
|
||||
if is_error:
|
||||
cursor.insertHtml(f'<font color="red">{text}</font><br>')
|
||||
else:
|
||||
formatted_text = f"{text}\n" if add_newline else text
|
||||
cursor.insertText(formatted_text)
|
||||
|
||||
self.log_widget.ensureCursorVisible()
|
||||
|
||||
def _reset_state(self):
|
||||
"""Сбрасывает состояние обработчика."""
|
||||
self.last_line_was_progress = False
|
||||
self.output_buffer = ""
|
||||
|
||||
def clear(self):
|
||||
"""Очищает лог и сбрасывает состояние."""
|
||||
if self.log_widget:
|
||||
self.log_widget.clear()
|
||||
self._reset_state()
|
||||
|
||||
def append_error(self, text):
|
||||
"""
|
||||
Добавляет сообщение об ошибке в лог.
|
||||
|
||||
Args:
|
||||
text: Текст ошибки
|
||||
"""
|
||||
self._append_to_log(text, is_error=True, add_newline=True)
|
||||
|
||||
def append_info(self, text, add_newline=True):
|
||||
"""
|
||||
Добавляет информационное сообщение в лог.
|
||||
|
||||
Args:
|
||||
text: Текст сообщения
|
||||
add_newline: True если добавить перенос строки
|
||||
"""
|
||||
self._append_to_log(text, is_error=False, add_newline=add_newline)
|
||||
|
||||
|
||||
class MultiLogHandler:
|
||||
"""
|
||||
Обработчик для управления несколькими LogProgressHandler.
|
||||
Полезен когда нужно обрабатывать логи для разных процессов одновременно.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Инициализирует хранилище обработчиков."""
|
||||
self.handlers = {}
|
||||
|
||||
def get_handler(self, name, log_widget):
|
||||
"""
|
||||
Получает или создает обработчик для указанного виджета.
|
||||
|
||||
Args:
|
||||
name: Имя обработчика (например, "install", "command")
|
||||
log_widget: Виджет QTextEdit для вывода
|
||||
|
||||
Returns:
|
||||
LogProgressHandler для указанного виджета
|
||||
"""
|
||||
if name not in self.handlers:
|
||||
self.handlers[name] = LogProgressHandler(log_widget)
|
||||
else:
|
||||
self.handlers[name].set_log_widget(log_widget)
|
||||
return self.handlers[name]
|
||||
|
||||
def get_handler_or_none(self, name):
|
||||
"""
|
||||
Получает обработчик по имени или None если не существует.
|
||||
|
||||
Args:
|
||||
name: Имя обработчика
|
||||
|
||||
Returns:
|
||||
LogProgressHandler или None
|
||||
"""
|
||||
return self.handlers.get(name)
|
||||
|
||||
def remove_handler(self, name):
|
||||
"""
|
||||
Удаляет обработчик по имени.
|
||||
|
||||
Args:
|
||||
name: Имя удаляемого обработчика
|
||||
"""
|
||||
if name in self.handlers:
|
||||
del self.handlers[name]
|
||||
|
||||
def clear_all(self):
|
||||
"""Очищает все обработчики."""
|
||||
for handler in self.handlers.values():
|
||||
handler.clear()
|
||||
self.handlers.clear()
|
||||
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
Утилиты для работы с процессами QProcess.
|
||||
|
||||
Устраняет дублирование кода остановки процессов в разных частях приложения.
|
||||
"""
|
||||
from PyQt5.QtCore import QProcess
|
||||
|
||||
|
||||
class ProcessUtils:
|
||||
"""Утилиты для управления QProcess."""
|
||||
|
||||
# Таймаут ожидания после terminate (мс)
|
||||
DEFAULT_TERMINATE_TIMEOUT = 3000
|
||||
|
||||
@staticmethod
|
||||
def stop_process(process: QProcess, timeout: int = DEFAULT_TERMINATE_TIMEOUT) -> None:
|
||||
"""
|
||||
Останавливает QProcess: сначала terminate, потом kill если не завершился.
|
||||
|
||||
Args:
|
||||
process: Процесс для остановки
|
||||
timeout: Время ожидания после terminate (мс)
|
||||
"""
|
||||
if not process:
|
||||
return
|
||||
|
||||
if process.state() == QProcess.Running:
|
||||
process.terminate()
|
||||
if not process.waitForFinished(timeout):
|
||||
process.kill()
|
||||
|
||||
@staticmethod
|
||||
def is_running(process: QProcess) -> bool:
|
||||
"""
|
||||
Проверяет, запущен ли процесс.
|
||||
|
||||
Args:
|
||||
process: Процесс для проверки
|
||||
|
||||
Returns:
|
||||
True если процесс запущен
|
||||
"""
|
||||
return process is not None and process.state() == QProcess.Running
|
||||
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
Виджеты для winehelper GUI.
|
||||
"""
|
||||
from winehelperqt.widgets.icon_manager import IconManager
|
||||
|
||||
__all__ = [
|
||||
'IconManager',
|
||||
]
|
||||
@@ -0,0 +1,307 @@
|
||||
"""
|
||||
Сервис для управления иконками и анимациями в WineHelper GUI.
|
||||
|
||||
Предоставляет функциональность:
|
||||
- Создание анимированных кнопок с иконками
|
||||
- Плавные переходы между иконками (fade анимация)
|
||||
- Создание цветных иконок (для тёмных/светлых тем)
|
||||
- Кэширование иконок
|
||||
- Извлечение иконок из .exe файлов
|
||||
"""
|
||||
import os
|
||||
from typing import List, Optional, Dict, Tuple
|
||||
|
||||
from PyQt5.QtCore import Qt, QObject, QTimer, QSize, QPropertyAnimation, QEasingCurve
|
||||
from PyQt5.QtGui import QIcon, QPixmap, QPainter, QColor
|
||||
from PyQt5.QtWidgets import QToolButton, QSizePolicy, QFrame, QVBoxLayout
|
||||
|
||||
|
||||
class IconManager(QObject):
|
||||
"""
|
||||
Сервис для управления иконками и анимациями.
|
||||
|
||||
Примерты использования:
|
||||
manager = IconManager(parent_widget)
|
||||
button = manager.create_animated_button(
|
||||
text="My App",
|
||||
icon_paths=["/path/to/icon1.png", "/path/to/icon2.png"],
|
||||
style_sheet="QToolButton { ... }"
|
||||
)
|
||||
"""
|
||||
|
||||
# Интервал смены иконок (мс)
|
||||
DEFAULT_ICON_INTERVAL = 2500
|
||||
# Длительность анимации fade (мс)
|
||||
FADE_DURATION = 700
|
||||
|
||||
def __init__(self, parent: Optional[QObject] = None):
|
||||
"""
|
||||
Инициализирует менеджер иконок.
|
||||
|
||||
Args:
|
||||
parent: Родительский объект
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self._icon_animators: Dict[QToolButton, Dict] = {}
|
||||
self._icon_cache: Dict[str, QIcon] = {}
|
||||
|
||||
def create_animated_button(self, text: str,
|
||||
icon_paths: List[str],
|
||||
style_sheet: str,
|
||||
icon_size: int = 32,
|
||||
min_width: int = 250,
|
||||
height: int = 70) -> QToolButton:
|
||||
"""
|
||||
Создает кнопку с анимацией смены иконок.
|
||||
|
||||
Args:
|
||||
text: Текст кнопки
|
||||
icon_paths: Пути к иконкам
|
||||
style_sheet: Стили для кнопки
|
||||
icon_size: Размер иконки
|
||||
min_width: Минимальная ширина
|
||||
height: Высота кнопки
|
||||
|
||||
Returns:
|
||||
Настроенная кнопка
|
||||
"""
|
||||
# Создаём кнопку
|
||||
btn = QToolButton()
|
||||
btn.setText(text)
|
||||
btn.setStyleSheet(style_sheet)
|
||||
btn.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
|
||||
btn.setFocusPolicy(Qt.NoFocus)
|
||||
btn.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
|
||||
btn.setMinimumWidth(min_width)
|
||||
btn.setFixedHeight(height)
|
||||
btn.setIconSize(QSize(icon_size, icon_size))
|
||||
|
||||
# Фильтруем существующие пути
|
||||
existing_paths = [p for p in icon_paths if p and os.path.exists(p)]
|
||||
|
||||
if existing_paths:
|
||||
if len(existing_paths) > 1:
|
||||
# Устанавливаем первую иконку и запускаем анимацию
|
||||
btn.setIcon(QIcon(existing_paths[0]))
|
||||
|
||||
# Создаём таймер для смены иконок
|
||||
main_timer = QTimer(btn)
|
||||
self._icon_animators[btn] = {
|
||||
'main_timer': main_timer,
|
||||
'icons': existing_paths,
|
||||
'current_index': 0,
|
||||
'animation': None
|
||||
}
|
||||
main_timer.timeout.connect(
|
||||
lambda b=btn: self._start_fade_animation(b)
|
||||
)
|
||||
main_timer.start(self.DEFAULT_ICON_INTERVAL)
|
||||
else:
|
||||
# Одна иконка без анимации
|
||||
btn.setIcon(QIcon(existing_paths[0]))
|
||||
else:
|
||||
# Пустая иконка для сохранения отступов
|
||||
btn.setIcon(QIcon())
|
||||
|
||||
return btn
|
||||
|
||||
def create_button_with_frame(self, text: str,
|
||||
icon_paths: List[str],
|
||||
style_sheet: str,
|
||||
frame_style: str) -> Tuple[QFrame, QToolButton]:
|
||||
"""
|
||||
Создает кнопку в рамке для выделения.
|
||||
|
||||
Args:
|
||||
text: Текст кнопки
|
||||
icon_paths: Пути к иконкам
|
||||
style_sheet: Стили для кнопки
|
||||
frame_style: Стили для рамки
|
||||
|
||||
Returns:
|
||||
Кортеж (рамка, кнопка)
|
||||
"""
|
||||
btn = self.create_animated_button(text, icon_paths, style_sheet)
|
||||
|
||||
frame = QFrame()
|
||||
frame.setStyleSheet(frame_style)
|
||||
layout = QVBoxLayout(frame)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(btn)
|
||||
|
||||
return frame, btn
|
||||
|
||||
def create_colorized_icon(self, icon_path: str, color: str) -> QIcon:
|
||||
"""
|
||||
Загружает иконку (SVG) и применяет указанный цвет.
|
||||
|
||||
Args:
|
||||
icon_path: Путь к иконке
|
||||
color: Цвет в формате Qt/Hex
|
||||
|
||||
Returns:
|
||||
Перекрашенная иконка
|
||||
"""
|
||||
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 cache_icon(self, key: str, icon: QIcon) -> None:
|
||||
"""
|
||||
Сохраняет иконку в кэш.
|
||||
|
||||
Args:
|
||||
key: Ключ кэша
|
||||
icon: Иконка для сохранения
|
||||
"""
|
||||
self._icon_cache[key] = icon
|
||||
|
||||
def get_cached_icon(self, key: str) -> Optional[QIcon]:
|
||||
"""
|
||||
Получает иконку из кэша.
|
||||
|
||||
Args:
|
||||
key: Ключ кэша
|
||||
|
||||
Returns:
|
||||
Иконка или None
|
||||
"""
|
||||
return self._icon_cache.get(key)
|
||||
|
||||
def clear_icon_cache(self) -> None:
|
||||
"""Очищает кэш иконок."""
|
||||
self._icon_cache.clear()
|
||||
|
||||
def set_selection_style(self, frame: QFrame, selected: bool) -> None:
|
||||
"""
|
||||
Устанавливает стиль выделения для рамки кнопки.
|
||||
|
||||
Args:
|
||||
frame: Рамка кнопки
|
||||
selected: True для выделения
|
||||
"""
|
||||
if selected:
|
||||
frame.setStyleSheet(
|
||||
"QFrame { border: 2px solid #0078d7; border-radius: 8px; "
|
||||
"padding: 0px; background-color: palette(window); }"
|
||||
)
|
||||
else:
|
||||
frame.setStyleSheet(
|
||||
"QFrame { border: 2px solid transparent; border-radius: 8px; "
|
||||
"padding: 0px; background-color: palette(window); }"
|
||||
)
|
||||
|
||||
def stop_button_animation(self, button: QToolButton) -> None:
|
||||
"""
|
||||
Останавливает анимацию для кнопки.
|
||||
|
||||
Args:
|
||||
button: Кнопка для остановки анимации
|
||||
"""
|
||||
if button in self._icon_animators:
|
||||
anim_data = self._icon_animators.pop(button)
|
||||
if 'main_timer' in anim_data:
|
||||
anim_data['main_timer'].stop()
|
||||
if 'animation' in anim_data and anim_data['animation']:
|
||||
anim_data['animation'].stop()
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""Останавливает все анимации и очищает ресурсы."""
|
||||
for button in list(self._icon_animators.keys()):
|
||||
self.stop_button_animation(button)
|
||||
self.clear_icon_cache()
|
||||
|
||||
def _start_fade_animation(self, button: QToolButton) -> None:
|
||||
"""
|
||||
Запускает анимацию плавного перехода между иконками.
|
||||
|
||||
Args:
|
||||
button: Кнопка для анимации
|
||||
"""
|
||||
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(self.FADE_DURATION)
|
||||
animation.setEasingCurve(QEasingCurve.InOutQuad)
|
||||
|
||||
animation.valueChanged.connect(
|
||||
lambda value, b=button: self._update_icon_blend(b, value)
|
||||
)
|
||||
animation.finished.connect(
|
||||
lambda b=button: self._on_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()
|
||||
|
||||
def _on_animation_finished(self, button: QToolButton) -> None:
|
||||
"""Обработчик завершения анимации."""
|
||||
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_blend(self, button: QToolButton, progress: float) -> None:
|
||||
"""
|
||||
Обновляет кадр анимации, смешивая две иконки.
|
||||
|
||||
Args:
|
||||
button: Кнопка
|
||||
progress: Прогресс от 0.0 до 1.0
|
||||
"""
|
||||
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))
|
||||