diff --git a/winehelper b/winehelper index a48f3be..0b91476 100755 --- a/winehelper +++ b/winehelper @@ -776,9 +776,10 @@ init_wined3d () { init_dxvk () { check_variables USE_DXVK_VER "$1" - get_dxvk () { - DXVK_URL="$1" - DXVK_PACKAGE="${WH_VULKAN_LIBDIR}/dxvk-${DXVK_VAR_VER}.tar.$(echo ${DXVK_URL#*.tar.})" + get_dxvk() { + local DXVK_URL="$1" + local DXVK_VAR_VER="$2" + local DXVK_PACKAGE="${WH_VULKAN_LIBDIR}/${DXVK_VAR_VER}.tar.$(echo "${DXVK_URL#*.tar.}")" if try_download "$DXVK_URL" "$DXVK_PACKAGE" check256sum \ && unpack "$DXVK_PACKAGE" "$WH_VULKAN_LIBDIR" then @@ -789,8 +790,8 @@ init_dxvk () { } for DXVK_VAR_VER in "$USE_DXVK_VER" $@ ; do - if [[ ! -d "${WH_VULKAN_LIBDIR}/dxvk-$DXVK_VAR_VER" ]] ; then - get_dxvk "$CLOUD_URL/dxvk-${DXVK_VAR_VER}.tar.xz" + if [[ ! -d "${WH_VULKAN_LIBDIR}/${DXVK_VAR_VER}" ]] ; then + get_dxvk "$CLOUD_URL/${DXVK_VAR_VER}.tar.xz" "$DXVK_VAR_VER" fi done @@ -803,8 +804,8 @@ init_dxvk () { fi for dxvkfiles in $DXVK_FILES ; do - try_copy_other_dll_to_pfx_64 "${WH_VULKAN_LIBDIR}/dxvk-$USE_DXVK_VER/x64/$dxvkfiles.dll" - if try_copy_other_dll_to_pfx_32 "${WH_VULKAN_LIBDIR}/dxvk-$USE_DXVK_VER/x32/$dxvkfiles.dll" + try_copy_other_dll_to_pfx_64 "${WH_VULKAN_LIBDIR}/${USE_DXVK_VER}/x64/$dxvkfiles.dll" + if try_copy_other_dll_to_pfx_32 "${WH_VULKAN_LIBDIR}/${USE_DXVK_VER}/x32/$dxvkfiles.dll" then var_winedlloverride_update "$dxvkfiles=n" fi done @@ -813,9 +814,10 @@ init_dxvk () { init_vkd3d () { check_variables USE_VKD3D_VER "$1" - get_vkd3d () { - VKD3D_URL="$1" - VKD3D_PACKAGE="${WH_VULKAN_LIBDIR}/vkd3d-proton-${VKD3D_VAR_VER}.tar.$(echo ${VKD3D_URL#*.tar.})" + get_vkd3d() { + local VKD3D_URL="$1" + local VKD3D_VAR_VER="$2" + local VKD3D_PACKAGE="${WH_VULKAN_LIBDIR}/${VKD3D_VAR_VER}.tar.$(echo "${VKD3D_URL#*.tar.}")" if try_download "$VKD3D_URL" "$VKD3D_PACKAGE" check256sum \ && unpack "$VKD3D_PACKAGE" "$WH_VULKAN_LIBDIR" then @@ -826,15 +828,15 @@ init_vkd3d () { } for VKD3D_VAR_VER in "$USE_VKD3D_VER" $@ ; do - if [[ ! -d "${WH_VULKAN_LIBDIR}/vkd3d-proton-$VKD3D_VAR_VER" ]] ; then - get_vkd3d "$CLOUD_URL/vkd3d-proton-${VKD3D_VAR_VER}.tar.xz" + if [[ ! -d "${WH_VULKAN_LIBDIR}/${VKD3D_VAR_VER}" ]] ; then + get_vkd3d "$CLOUD_URL/${VKD3D_VAR_VER}.tar.xz" "$VKD3D_VAR_VER" fi done VKD3D_FILES="d3d12 d3d12core libvkd3d-shader-1 libvkd3d-1" # libvkd3d-proton-utils-3 for vkd3dfiles in $VKD3D_FILES ; do - try_copy_other_dll_to_pfx_64 "${WH_VULKAN_LIBDIR}/vkd3d-proton-$USE_VKD3D_VER/x64/$vkd3dfiles.dll" - if try_copy_other_dll_to_pfx_32 "${WH_VULKAN_LIBDIR}/vkd3d-proton-$USE_VKD3D_VER/x86/$vkd3dfiles.dll" + try_copy_other_dll_to_pfx_64 "${WH_VULKAN_LIBDIR}/${USE_VKD3D_VER}/x64/$vkd3dfiles.dll" + if try_copy_other_dll_to_pfx_32 "${WH_VULKAN_LIBDIR}/${USE_VKD3D_VER}/x86/$vkd3dfiles.dll" then var_winedlloverride_update "$vkd3dfiles=n" fi done @@ -2011,6 +2013,57 @@ restore_prefix() { return 0 } +update_last_conf_var() { + local var_name="$1" + local new_value="$2" + local conf_file="$WINEPREFIX/last.conf" + + if [[ ! -f "$conf_file" ]]; then + print_warning "Файл last.conf не найден, не могу обновить переменную $var_name." + return 1 + fi + + if grep -q "export $var_name=" "$conf_file"; then + sed -i "s|^export $var_name=.*|export $var_name=\"$new_value\"|" "$conf_file" + else + echo "export $var_name=\"$new_value\"" >> "$conf_file" + fi +} + +run_install_dxvk() { + local version="$1" + check_prefix_var + init_database + init_wine_ver + init_wineprefix + if [[ "$version" == "none" ]] ; then + print_info "Удаление DXVK..." + init_wined3d + update_last_conf_var "DXVK_VER" "" + else + init_dxvk "$version" + update_last_conf_var "DXVK_VER" "$USE_DXVK_VER" + fi + wait_wineserver +} + +run_install_vkd3d() { + local version="$1" + check_prefix_var + init_database + init_wine_ver + init_wineprefix + if [[ "$version" == "none" ]] ; then + print_info "Удаление VKD3D..." + init_wined3d + update_last_conf_var "VKD3D_VER" "" + else + init_vkd3d "$version" + update_last_conf_var "VKD3D_VER" "$USE_VKD3D_VER" + fi + wait_wineserver +} + wh_info () { echo "Использование: $SCRIPT_NAME [команда] @@ -2019,6 +2072,9 @@ wh_info () { install [скрипт] запустить скрипт установки программы install [скрипт] --clear-pfx не использовать готовый префикс для установки ПО + install-dxvk [версия|none] установить/удалить DXVK в выбранный префикс + install-vkd3d [версия|none] установить/удалить VKD3D в выбранный префикс + installed список установленных программ run [программа] запуск программы (отладка) remove-all удалить WineHelper и все связанные данные @@ -2066,6 +2122,8 @@ case "$arg1" in winetricks) prepair_wine ; "$WH_WINETRICKS" -q "$@" ;; desktop) create_desktop "$@" ; exit 0 ;; install|-i) run_autoinstall "$@" ;; + install-dxvk) run_install_dxvk "$@" ;; + install-vkd3d) run_install_vkd3d "$@" ;; installed) check_installed_programs "$1" ;; run|-r) run_installed_programs "$1" ;; backup-prefix) backup_prefix "$@" ;; diff --git a/winehelper_gui.py b/winehelper_gui.py index 401d0b5..1897e5e 100644 --- a/winehelper_gui.py +++ b/winehelper_gui.py @@ -1078,7 +1078,7 @@ class WineVersionSelectionDialog(QDialog): if not line: continue - match = re.match(r'^#+\s+([A-Z_]+)\s+#*$', line) + match = re.match(r'^#+\s*([^#]+?)\s*#*$', line) if match: group_name = match.group(1) allowed_groups = {"WINE", "WINE_LG", "PROTON_LG", "PROTON_STEAM"} @@ -1354,6 +1354,118 @@ class CreatePrefixDialog(QDialog): self.accept() +class ComponentVersionSelectionDialog(QDialog): + """Диалог для выбора версии компонента (DXVK, VKD3D).""" + + def __init__(self, component_group, title, parent=None, add_extra_options=True): + super().__init__(parent) + self.component_group = component_group + self.selected_version = None + self.versions_data = [] + + self.setWindowTitle(title) + self.setMinimumSize(600, 400) + self.setModal(True) + + main_layout = QVBoxLayout(self) + + self.search_edit = QLineEdit() + self.search_edit.setPlaceholderText("Поиск версии...") + self.search_edit.textChanged.connect(self.filter_versions) + main_layout.addWidget(self.search_edit) + + self.scroll_area = QScrollArea() + self.scroll_area.setWidgetResizable(True) + 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.buttons = [] + # Кнопка "Удалить" теперь находится вне сетки, поэтому начинаем с 0 строки. + self.load_versions(start_row=0) + + # --- Панель с кнопками действий внизу диалога --- + button_layout = QHBoxLayout() + + if add_extra_options: + uninstall_btn = QPushButton("Удалить из префикса") + uninstall_btn.setToolTip("Удаляет текущую установленную версию компонента из префикса.") + uninstall_btn.clicked.connect(partial(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) + + main_layout.addLayout(button_layout) + + def load_versions(self, start_row): + """Загружает и отображает версии.""" + self._parse_sha256_list() + self.populate_ui(start_row) + + def _parse_sha256_list(self): + """Парсит sha256sum.list для получения списка версий.""" + sha256_path = os.path.join(Var.DATA_PATH, "sha256sum.list") + if not os.path.exists(sha256_path): + self.versions_data = [] + return + + 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 = re.match(r'^#+\s*([^#]+?)\s*#*$', line) + if match: + current_group = match.group(1).strip() + continue + if current_group == self.component_group and re.match(r'^[a-f0-9]{64}', line): + parts = line.split(maxsplit=1) + if len(parts) == 2: + filename = parts[1] + if filename.endswith('.tar.xz'): + version_name = filename[:-7] + self.versions_data.append(version_name) + except IOError: + self.versions_data = [] + + def populate_ui(self, start_row): + """Заполняет UI кнопками версий.""" + versions = sorted(self.versions_data, reverse=True) + num_columns = 3 + 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 >= num_columns: + col = 0 + row += 1 + + def filter_versions(self): + """Фильтрует видимость кнопок версий на основе текста поиска.""" + search_text = self.search_edit.text().lower() + for btn_widget in self.buttons: + is_match = search_text in btn_widget.text().lower() + btn_widget.setVisible(is_match) + + def on_version_selected(self, version_name): + """Обрабатывает выбор версии.""" + self.selected_version = version_name + self.accept() + class WineHelperGUI(QMainWindow): def __init__(self): super().__init__() @@ -1980,11 +2092,29 @@ class WineHelperGUI(QMainWindow): self.prefix_winefile_button.setToolTip("Запуск файлового менеджера Wine (winefile) для просмотра файлов внутри префикса.") management_layout.addWidget(self.prefix_winefile_button, 2, 1) + # Добавляем небольшой отступ + spacer_widget = QWidget() + spacer_widget.setFixedHeight(5) + management_layout.addWidget(spacer_widget, 3, 0, 1, 2) + + self.dxvk_manage_button = QPushButton("Управление DXVK") + self.dxvk_manage_button.setMinimumHeight(32) + self.dxvk_manage_button.clicked.connect(lambda: self.open_component_version_manager('dxvk')) + self.dxvk_manage_button.setToolTip("Установка или удаление определенной версии DXVK в префиксе.") + management_layout.addWidget(self.dxvk_manage_button, 4, 0) + + self.vkd3d_manage_button = QPushButton("Управление VKD3D") + self.vkd3d_manage_button.setMinimumHeight(32) + self.vkd3d_manage_button.clicked.connect(lambda: self.open_component_version_manager('vkd3d-proton')) + self.vkd3d_manage_button.setToolTip("Установка или удаление определенной версии vkd3d-proton в префиксе.") + management_layout.addWidget(self.vkd3d_manage_button, 4, 1) + # --- Правая сторона: Информационный блок --- self.prefix_info_display = QTextBrowser() self.prefix_info_display.setReadOnly(True) self.prefix_info_display.setFrameStyle(QFrame.StyledPanel) - management_layout.addWidget(self.prefix_info_display, 0, 2, 3, 1) + # Увеличиваем rowspan, чтобы учесть добавленный отступ + management_layout.addWidget(self.prefix_info_display, 0, 2, 5, 1) management_layout.setColumnStretch(0, 1) management_layout.setColumnStretch(1, 1) @@ -1994,7 +2124,7 @@ class WineHelperGUI(QMainWindow): separator = QFrame() separator.setFrameShape(QFrame.HLine) separator.setFrameShadow(QFrame.Sunken) - management_layout.addWidget(separator, 3, 0, 1, 3) + management_layout.addWidget(separator, 5, 0, 1, 3) install_group = QWidget() install_layout = QVBoxLayout(install_group) @@ -2027,7 +2157,7 @@ class WineHelperGUI(QMainWindow): action_buttons_layout.addWidget(self.create_launcher_button) install_layout.addLayout(action_buttons_layout) - management_layout.addWidget(install_group, 4, 0, 1, 3) + management_layout.addWidget(install_group, 6, 0, 1, 3) container_layout.addWidget(self.prefix_management_groupbox) layout.addWidget(self.management_container_groupbox) @@ -2220,8 +2350,10 @@ class WineHelperGUI(QMainWindow): "WINEARCH": ("Архитектура", lambda v: "64-bit" if v == "win64" else "32-bit"), "WH_WINE_USE": ("Версия Wine", lambda v: "Системная" if v == "system" else v), "BASE_PFX": ("Тип", lambda v: 'Чистый' if v == "none" else 'С рекомендуемыми библиотеками'), + "DXVK_VER": ("Версия DXVK", lambda v: v if v else "Не установлено"), + "VKD3D_VER": ("Версия VKD3D", lambda v: v if v else "Не установлено"), } - display_order = ["WINEPREFIX", "WINEARCH", "WH_WINE_USE", "BASE_PFX"] + display_order = ["WINEPREFIX", "WINEARCH", "WH_WINE_USE", "BASE_PFX", "DXVK_VER", "VKD3D_VER"] html_content = f'

' html_content += f"Имя: {html.escape(prefix_name)}
" @@ -2310,6 +2442,128 @@ class WineHelperGUI(QMainWindow): self.command_process.start(wine_executable, args) self.command_dialog.exec_() + def _get_prefix_component_version(self, prefix_name, component_key): + """ + Читает last.conf префикса и возвращает версию указанного компонента. + :param prefix_name: Имя префикса. + :param component_key: Ключ компонента (например, 'DXVK_VER'). + :return: Строку с версией или None, если не найдено или значение пустое. + """ + if not prefix_name: + return None + + last_conf_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name, "last.conf") + if not os.path.exists(last_conf_path): + return None + + try: + with open(last_conf_path, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + # Ищем строку вида 'export KEY="value"' или 'export KEY=value' + if line.startswith('export '): + parts = line[7:].split('=', 1) + if len(parts) == 2: + key = parts[0].strip() + if key == component_key: + value = parts[1].strip().strip('"\'') + # Возвращаем значение, только если оно не пустое. + return value if value else None + except IOError as e: + print(f"Ошибка чтения last.conf для {prefix_name}: {e}") + return None + return None + + def open_component_version_manager(self, component): + """Открывает диалог выбора версии для DXVK/VKD3D и запускает установку.""" + prefix_name = self.current_managed_prefix_name + if not prefix_name: + QMessageBox.warning(self, "Ошибка", "Сначала выберите префикс.") + return + + component_key = None + if component == 'dxvk': + group = 'DXVK' + title = f"Управление DXVK для префикса: {prefix_name}" + command = 'install-dxvk' + component_key = 'DXVK_VER' + elif component == 'vkd3d-proton': + group = 'VKD3D' + title = f"Управление vkd3d для префикса: {prefix_name}" + command = 'install-vkd3d' + component_key = 'VKD3D_VER' + else: + return + + dialog = ComponentVersionSelectionDialog(group, title, self) + if dialog.exec_() == QDialog.Accepted and dialog.selected_version: + version = dialog.selected_version + + if version == "none": + # Удаление: сначала проверяем, есть ли что удалять. + installed_version = self._get_prefix_component_version(prefix_name, component_key) + if not installed_version: + QMessageBox.information(self, "Информация", "Установленных версий нет.") + return # Прерываем выполнение, т.к. удалять нечего + # Для удаления лицензия не нужна, запускаем сразу. + self.run_component_install_command(prefix_name, command, version) + else: + # Установка: сначала показываем лицензионное соглашение. + if not self._show_license_agreement_dialog(): + return # Пользователь отклонил лицензию + + # Если лицензия принята, запускаем установку. + self.run_component_install_command(prefix_name, command, version) + + def run_component_install_command(self, prefix_name, command, version): + """Выполняет команду установки компонента (DXVK/VKD3D) через winehelper.""" + prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name) + + self.command_dialog = QDialog(self) + self.command_dialog.setWindowTitle(f"Выполнение: {command} {version}") + self.command_dialog.setMinimumSize(750, 400) + self.command_dialog.setModal(True) + self.command_dialog.setWindowFlags(self.command_dialog.windowFlags() & ~Qt.WindowCloseButtonHint) + + layout = QVBoxLayout() + self.command_log_output = QTextEdit() + self.command_log_output.setReadOnly(True) + self.command_log_output.setFont(QFont('DejaVu Sans Mono', 10)) + layout.addWidget(self.command_log_output) + + self.command_close_button = QPushButton("Закрыть") + self.command_close_button.setEnabled(False) + self.command_close_button.clicked.connect(self.command_dialog.close) + layout.addWidget(self.command_close_button) + self.command_dialog.setLayout(layout) + + self.command_process = QProcess(self.command_dialog) + self.command_process.setProcessChannelMode(QProcess.MergedChannels) + self.command_process.readyReadStandardOutput.connect(self._handle_command_output) + self.command_process.finished.connect( + lambda exit_code, exit_status: self._handle_component_install_finished( + prefix_name, exit_code, exit_status + ) + ) + + env = QProcessEnvironment.systemEnvironment() + env.insert("WINEPREFIX", prefix_path) + self.command_process.setProcessEnvironment(env) + + args = [command, version] + self.command_log_output.append(f"Выполнение: {shlex.quote(self.winehelper_path)} {' '.join(shlex.quote(a) for a in args)}") + self.command_process.start(self.winehelper_path, args) + self.command_dialog.exec_() + + def _handle_component_install_finished(self, prefix_name, exit_code, exit_status): + """Обрабатывает завершение установки компонента и обновляет информацию о префиксе.""" + # Вызываем общий обработчик для обновления лога и кнопки закрытия + self._handle_command_finished(exit_code, exit_status) + + # В случае успеха обновляем панель информации о префиксе + if exit_code == 0: + self.update_prefix_info_display(prefix_name) + def create_launcher_for_prefix(self): """ Открывает диалог для создания ярлыка для приложения внутри выбранного префикса.