import os import shutil import tempfile from PySide6.QtGui import QPixmap from PySide6.QtWidgets import ( QDialog, QLineEdit, QFormLayout, QPushButton, QHBoxLayout, QDialogButtonBox, QFileDialog, QLabel ) from PySide6.QtCore import Qt from icoextract import IconExtractor, IconExtractorError from PIL import Image from portprotonqt.config_utils import get_portproton_location from portprotonqt.localization import _ from portprotonqt.logger import get_logger import portprotonqt.themes.standart.styles as default_styles logger = get_logger(__name__) def generate_thumbnail(inputfile, outfile, size=128, force_resize=True): """ Generates a thumbnail for an .exe file. inputfile: the input file path (%i) outfile: output filename (%o) size: determines the thumbnail output size (%s) """ logger.debug(f"Начинаем генерацию миниатюры: {inputfile} → {outfile}, размер={size}, принудительно={force_resize}") try: extractor = IconExtractor(inputfile) logger.debug("IconExtractor успешно создан.") except (RuntimeError, IconExtractorError) as e: logger.warning(f"Не удалось создать IconExtractor: {e}") return False try: data = extractor.get_icon() im = Image.open(data) logger.debug(f"Извлечена иконка размером {im.size}, форматы: {im.format}, кадры: {getattr(im, 'n_frames', 1)}") except Exception as e: logger.warning(f"Ошибка при извлечении иконки: {e}") return False if force_resize: logger.debug(f"Принудительное изменение размера иконки на {size}x{size}") im = im.resize((size, size)) else: if size > 256: logger.warning('Запрошен размер больше 256, установлен 256') size = 256 elif size not in (128, 256): logger.warning(f'Неподдерживаемый размер {size}, установлен 128') size = 128 if size == 256: logger.debug("Сохраняем иконку без изменения размера (256x256)") im.save(outfile, "PNG") logger.info(f"Иконка сохранена в {outfile}") return True frames = getattr(im, 'n_frames', 1) try: for frame in range(frames): im.seek(frame) if im.size == (size, size): logger.debug(f"Найден кадр с размером {size}x{size}") break except EOFError: logger.debug("Кадры закончились до нахождения нужного размера.") if im.size != (size, size): logger.debug(f"Изменение размера с {im.size} на {size}x{size}") im = im.resize((size, size)) try: im.save(outfile, "PNG") logger.info(f"Миниатюра успешно сохранена в {outfile}") return True except Exception as e: logger.error(f"Ошибка при сохранении миниатюры: {e}") return False class AddGameDialog(QDialog): def __init__(self, parent=None, theme=None, edit_mode=False, game_name=None, exe_path=None, cover_path=None): super().__init__(parent) self.theme = theme if theme else default_styles self.edit_mode = edit_mode self.original_name = game_name self.setWindowTitle(_("Edit Game") if edit_mode else _("Add Game")) self.setModal(True) self.setStyleSheet(self.theme.MAIN_WINDOW_STYLE + self.theme.MESSAGE_BOX_STYLE) layout = QFormLayout(self) # Game name self.nameEdit = QLineEdit(self) self.nameEdit.setStyleSheet(self.theme.SEARCH_EDIT_STYLE + " QLineEdit { color: #ffffff; font-size: 14px; }") if game_name: self.nameEdit.setText(game_name) name_label = QLabel(_("Game Name:")) name_label.setStyleSheet(self.theme.PARAMS_TITLE_STYLE + " QLabel { color: #ffffff; font-size: 14px; font-weight: bold; }") layout.addRow(name_label, self.nameEdit) # Exe path self.exeEdit = QLineEdit(self) self.exeEdit.setStyleSheet(self.theme.SEARCH_EDIT_STYLE + " QLineEdit { color: #ffffff; font-size: 14px; }") if exe_path: self.exeEdit.setText(exe_path) exeBrowseButton = QPushButton(_("Browse..."), self) exeBrowseButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) exeBrowseButton.clicked.connect(self.browseExe) exeLayout = QHBoxLayout() exeLayout.addWidget(self.exeEdit) exeLayout.addWidget(exeBrowseButton) exe_label = QLabel(_("Path to Executable:")) exe_label.setStyleSheet(self.theme.PARAMS_TITLE_STYLE + " QLabel { color: #ffffff; font-size: 14px; font-weight: bold; }") layout.addRow(exe_label, exeLayout) # Cover path self.coverEdit = QLineEdit(self) self.coverEdit.setStyleSheet(self.theme.SEARCH_EDIT_STYLE + " QLineEdit { color: #ffffff; font-size: 14px; }") if cover_path: self.coverEdit.setText(cover_path) coverBrowseButton = QPushButton(_("Browse..."), self) coverBrowseButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) coverBrowseButton.clicked.connect(self.browseCover) coverLayout = QHBoxLayout() coverLayout.addWidget(self.coverEdit) coverLayout.addWidget(coverBrowseButton) cover_label = QLabel(_("Custom Cover:")) cover_label.setStyleSheet(self.theme.PARAMS_TITLE_STYLE + " QLabel { color: #ffffff; font-size: 14px; font-weight: bold; }") layout.addRow(cover_label, coverLayout) # Preview self.coverPreview = QLabel(self) self.coverPreview.setStyleSheet(self.theme.CONTENT_STYLE + " QLabel { color: #ffffff; }") preview_label = QLabel(_("Cover Preview:")) preview_label.setStyleSheet(self.theme.PARAMS_TITLE_STYLE + " QLabel { color: #ffffff; font-size: 14px; font-weight: bold; }") layout.addRow(preview_label, self.coverPreview) # Dialog buttons buttonBox = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) buttonBox.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) buttonBox.accepted.connect(self.accept) buttonBox.rejected.connect(self.reject) layout.addRow(buttonBox) self.coverEdit.textChanged.connect(self.updatePreview) self.exeEdit.textChanged.connect(self.updatePreview) if edit_mode: self.updatePreview() def browseExe(self): fileNameAndFilter = QFileDialog.getOpenFileName( self, _("Select Executable"), "", "Windows Executables (*.exe)" ) fileName = fileNameAndFilter[0] if fileName: self.exeEdit.setText(fileName) if not self.edit_mode: self.nameEdit.setText(os.path.splitext(os.path.basename(fileName))[0]) def browseCover(self): fileNameAndFilter = QFileDialog.getOpenFileName( self, _("Select Cover Image"), "", "Images (*.png *.jpg *.jpeg *.bmp)" ) fileName = fileNameAndFilter[0] if fileName: self.coverEdit.setText(fileName) def updatePreview(self): """Update the cover preview image.""" cover_path = self.coverEdit.text().strip() exe_path = self.exeEdit.text().strip() if cover_path and os.path.isfile(cover_path): pixmap = QPixmap(cover_path) if not pixmap.isNull(): self.coverPreview.setPixmap(pixmap.scaled(250, 250, Qt.AspectRatioMode.KeepAspectRatio)) else: self.coverPreview.setText(_("Invalid image")) elif os.path.isfile(exe_path): tmp = tempfile.NamedTemporaryFile(suffix='.png', delete=False) tmp.close() if generate_thumbnail(exe_path, tmp.name, size=128): pixmap = QPixmap(tmp.name) self.coverPreview.setPixmap(pixmap) os.unlink(tmp.name) else: self.coverPreview.setText(_("No cover selected")) def getDesktopEntryData(self): """Returns the .desktop content and save path""" exe_path = self.exeEdit.text().strip() name = self.nameEdit.text().strip() if not exe_path or not name: return None, None portproton_path = get_portproton_location() if portproton_path is None: return None, None is_flatpak = ".var" in portproton_path base_path = os.path.join(portproton_path, "data") if is_flatpak: exec_str = f'flatpak run ru.linux_gaming.PortProton "{exe_path}"' else: start_sh = os.path.join(base_path, "scripts", "start.sh") exec_str = f'env "{start_sh}" "{exe_path}"' icon_path = os.path.join(base_path, "img", f"{name}.png") desktop_path = os.path.join(portproton_path, f"{name}.desktop") working_dir = os.path.join(base_path, "scripts") user_cover_path = self.coverEdit.text().strip() if os.path.isfile(user_cover_path): shutil.copy(user_cover_path, icon_path) else: os.makedirs(os.path.dirname(icon_path), exist_ok=True) os.system(f'exe-thumbnailer "{exe_path}" "{icon_path}"') comment = _('Launch game "{name}" with PortProton').format(name=name) desktop_entry = f"""[Desktop Entry] Name={name} Comment={comment} Exec={exec_str} Terminal=false Type=Application Categories=Game; StartupNotify=true Path={working_dir} Icon={icon_path} """ return desktop_entry, desktop_path