11 Commits

Author SHA1 Message Date
e57770f796 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-06 17:58:30 +05:00
49cd77ee38 chore(localization): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-06 17:56:57 +05:00
d26b9774a0 feat(add_game): download cover if link is provided
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-06 17:54:53 +05:00
9a27d67dc0 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-06 17:14:29 +05:00
b0fff5af0c ci(pre-commit): exclude QSS themes from pyright and target them in qss check
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-06 17:11:35 +05:00
e54fac8aa4 feat: exclude custom_data from package
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-06 17:08:40 +05:00
f111674260 feat: rename launchers custom_data
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-06 17:05:07 +05:00
a5df7f0477 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-06 13:19:13 +05:00
f2954497d9 chore(readme): update todo
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-06 13:18:04 +05:00
80bbab692d chore(documentation): mention localization in custom data
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-06 13:17:07 +05:00
731e919884 feat: added translate support to custom data
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-07-06 13:10:37 +05:00
49 changed files with 154 additions and 68 deletions

View File

@@ -1,6 +1,6 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
exclude: '(data/|documentation/|portprotonqt/locales/|dev-scripts/|\.venv/|venv/|.*\.svg$)'
exclude: '(data/|documentation/|portprotonqt/locales/|portprotonqt/custom_data/|dev-scripts/|\.venv/|venv/|.*\.svg$)'
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
@@ -27,8 +27,9 @@ repos:
name: pyright
entry: pyright
language: system
'types_or': [python, pyi]
types_or: [python, pyi]
require_serial: true
exclude: '^portprotonqt/themes/[^/]+/styles\.py$'
- repo: local
hooks:
@@ -37,5 +38,5 @@ repos:
entry: ./dev-scripts/check_qss_properties.py
language: system
types: [file]
files: \.py$
files: ^portprotonqt/themes/[^/]+/styles\.py$
pass_filenames: false

View File

@@ -6,15 +6,16 @@
## [Unreleased]
### Added
- Переводы в переопределениях (за подробностями в документацию)
- Обложки и описания для всех автоинсталлов
- Возможность указать ссылку для скачивания обложки в диалоге добавления игры
### Changed
- Оптимизированны обложки автоинсталлов
- Папка custom_data исключена из сборки модуля для уменьшение его размера
### Fixed
### Contributors
- @Vector_null

View File

@@ -34,7 +34,7 @@
- [ ] Достигнуть паритета функциональности с PortProton
- [X] Добавить возможность изменения названия, описания и обложки через файлы `.local/share/PortProtonQT/custom_data/exe_name/{desc,name,cover}`
- [X] Добавить встроенное переопределение названия, описания и обложки, например, по пути `portprotonqt/custom_data` [Документация](documentation/metadata_override/)
- [ ] Добавить переводы в переопределения
- [X] Добавить переводы в переопределения
- [ ] Придумать как переопределять launcher.exe
- [X] Добавить в карточку игры сведения о поддержке геймпада
- [X] Добавить в карточки данные с ProtonDB

View File

@@ -20,9 +20,9 @@ Current translation status:
| Locale | Progress | Translated |
| :----- | -------: | ---------: |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 192 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 192 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 192 of 192 |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 194 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 194 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 194 of 194 |
---

View File

@@ -20,9 +20,9 @@
| Локаль | Прогресс | Переведено |
| :----- | -------: | ---------: |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 192 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 192 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 192 из 192 |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 194 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 194 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 194 из 194 |
---

View File

@@ -50,7 +50,9 @@ Each `<exe_name>` folder can include:
- `metadata.txt` — contains name and description:
```txt
name=My Game Title
name_ru=My Game Title (in russian language)
description=My Game Description
description_ru=My Game Description (in russian language)
```
- `cover.<extension>` — image file (`.png`, `.jpg`, `.jpeg`, `.bmp`)

View File

@@ -50,7 +50,9 @@
- `metadata.txt` — имя и описание в формате:
```txt
name=Моё название игры
description=Описание моей игры
name_en=Моё название игры (на английском)
description=Описание моей игры (на английском)
description_en=Описание моей игры
```
- `cover.<расширение>` — обложка (`.png`, `.jpg`, `.jpeg`, `.bmp`)

View File

Before

Width:  |  Height:  |  Size: 720 KiB

After

Width:  |  Height:  |  Size: 720 KiB

View File

Before

Width:  |  Height:  |  Size: 655 KiB

After

Width:  |  Height:  |  Size: 655 KiB

View File

Before

Width:  |  Height:  |  Size: 315 KiB

After

Width:  |  Height:  |  Size: 315 KiB

View File

Before

Width:  |  Height:  |  Size: 978 KiB

After

Width:  |  Height:  |  Size: 978 KiB

View File

Before

Width:  |  Height:  |  Size: 650 KiB

After

Width:  |  Height:  |  Size: 650 KiB

View File

Before

Width:  |  Height:  |  Size: 391 KiB

After

Width:  |  Height:  |  Size: 391 KiB

View File

Before

Width:  |  Height:  |  Size: 710 KiB

After

Width:  |  Height:  |  Size: 710 KiB

View File

Before

Width:  |  Height:  |  Size: 670 KiB

After

Width:  |  Height:  |  Size: 670 KiB

View File

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

Before

Width:  |  Height:  |  Size: 814 KiB

After

Width:  |  Height:  |  Size: 814 KiB

View File

Before

Width:  |  Height:  |  Size: 566 KiB

After

Width:  |  Height:  |  Size: 566 KiB

View File

Before

Width:  |  Height:  |  Size: 895 KiB

After

Width:  |  Height:  |  Size: 895 KiB

View File

Before

Width:  |  Height:  |  Size: 627 KiB

After

Width:  |  Height:  |  Size: 627 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 722 KiB

After

Width:  |  Height:  |  Size: 722 KiB

View File

@@ -1,5 +1,6 @@
import os
import tempfile
import re
from typing import cast, TYPE_CHECKING
from PySide6.QtGui import QPixmap, QIcon
from PySide6.QtWidgets import (
@@ -14,6 +15,7 @@ from portprotonqt.logger import get_logger
import portprotonqt.themes.standart.styles as default_styles
from portprotonqt.theme_manager import ThemeManager
from portprotonqt.custom_widgets import AutoSizeButton
from portprotonqt.downloader import Downloader
if TYPE_CHECKING:
from portprotonqt.main_window import MainWindow
@@ -449,6 +451,7 @@ class AddGameDialog(QDialog):
self.original_name = game_name
self.last_exe_path = exe_path # Store last selected exe path
self.last_cover_path = cover_path # Store last selected cover path
self.downloader = Downloader(max_workers=4) # Initialize Downloader
self.setWindowTitle(_("Edit Game") if edit_mode else _("Add Game"))
self.setModal(True)
@@ -472,8 +475,7 @@ class AddGameDialog(QDialog):
# Exe path
exe_label = QLabel(_("Path to Executable:"))
exe_label.setStyleSheet(
self.theme.PARAMS_TITLE_STYLE)
exe_label.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
self.exeEdit = CustomLineEdit(self, theme=self.theme)
self.exeEdit.setStyleSheet(self.theme.ADDGAME_INPUT_STYLE)
@@ -550,7 +552,7 @@ class AddGameDialog(QDialog):
exeBrowseButton.setFixedWidth(self.exeEdit.width())
coverBrowseButton.setFixedWidth(self.coverEdit.width())
# Вызываем после отображения окна, когда размеры установлены, чтобы реально дождаться, когда всё сформируется
# Вызываем после отображения окна, когда размеры установлены
QTimer.singleShot(0, update_button_widths)
# Обновляем превью, если в режиме редактирования
@@ -615,15 +617,46 @@ class AddGameDialog(QDialog):
"""Обработчик выбора файла обложки в FileExplorer"""
if file_path and os.path.splitext(file_path)[1].lower() in ('.png', '.jpg', '.jpeg', '.bmp'):
self.coverEdit.setText(file_path)
self.last_cover_path = file_path # Update last selected cover path
self.last_cover_path = file_path
self.updatePreview()
else:
logger.warning(f"Selected file is not a valid image: {file_path}")
def handleDownloadedCover(self, file_path):
"""Handle the downloaded cover image and update the preview."""
if file_path and os.path.isfile(file_path):
self.last_cover_path = file_path
pixmap = QPixmap(file_path)
if not pixmap.isNull():
self.coverPreview.setPixmap(pixmap.scaled(250, 250, Qt.AspectRatioMode.KeepAspectRatio))
else:
self.coverPreview.setText(_("Invalid image"))
else:
self.coverPreview.setText(_("Failed to download cover"))
logger.warning(f"Failed to download cover to {file_path}")
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):
# Check if cover_path is a URL
url_pattern = r'^https?://[^\s/$.?#].[^\s]*$'
if re.match(url_pattern, cover_path):
# Create a temporary file for the downloaded image
fd, local_path = tempfile.mkstemp(suffix=".png")
os.close(fd)
os.unlink(local_path)
# Start asynchronous download
self.downloader.download_async(
url=cover_path,
local_path=local_path,
timeout=10,
callback=self.handleDownloadedCover
)
self.coverPreview.setText(_("Downloading cover..."))
elif 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))
@@ -666,8 +699,8 @@ class AddGameDialog(QDialog):
os.makedirs(os.path.dirname(icon_path), exist_ok=True)
# Generate thumbnail (128x128) from exe
if not generate_thumbnail(exe_path, icon_path, size=128):
# Generate thumbnail (128x128) from exe if no cover is provided
if not self.last_cover_path and not generate_thumbnail(exe_path, icon_path, size=128):
logger.error(f"Failed to generate thumbnail from exe: {exe_path}")
icon_path = "" # Set empty icon if generation fails

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-07-03 19:29+0700\n"
"POT-Creation-Date: 2025-07-06 17:56+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: de_DE\n"
@@ -296,6 +296,12 @@ msgstr ""
msgid "Invalid image"
msgstr ""
msgid "Failed to download cover"
msgstr ""
msgid "Downloading cover..."
msgstr ""
msgid "No cover selected"
msgstr ""
@@ -338,6 +344,9 @@ msgstr ""
msgid "Pending"
msgstr ""
msgid "Unknown Game"
msgstr ""
msgid "Library"
msgstr ""
@@ -362,9 +371,6 @@ msgstr ""
msgid "Loading PortProton games..."
msgstr ""
msgid "Unknown Game"
msgstr ""
msgid "Game Library"
msgstr ""

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-07-03 19:29+0700\n"
"POT-Creation-Date: 2025-07-06 17:56+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: es_ES\n"
@@ -296,6 +296,12 @@ msgstr ""
msgid "Invalid image"
msgstr ""
msgid "Failed to download cover"
msgstr ""
msgid "Downloading cover..."
msgstr ""
msgid "No cover selected"
msgstr ""
@@ -338,6 +344,9 @@ msgstr ""
msgid "Pending"
msgstr ""
msgid "Unknown Game"
msgstr ""
msgid "Library"
msgstr ""
@@ -362,9 +371,6 @@ msgstr ""
msgid "Loading PortProton games..."
msgstr ""
msgid "Unknown Game"
msgstr ""
msgid "Game Library"
msgstr ""

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PortProtonQt 0.1.1\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-07-03 19:29+0700\n"
"POT-Creation-Date: 2025-07-06 17:56+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -294,6 +294,12 @@ msgstr ""
msgid "Invalid image"
msgstr ""
msgid "Failed to download cover"
msgstr ""
msgid "Downloading cover..."
msgstr ""
msgid "No cover selected"
msgstr ""
@@ -336,6 +342,9 @@ msgstr ""
msgid "Pending"
msgstr ""
msgid "Unknown Game"
msgstr ""
msgid "Library"
msgstr ""
@@ -360,9 +369,6 @@ msgstr ""
msgid "Loading PortProton games..."
msgstr ""
msgid "Unknown Game"
msgstr ""
msgid "Game Library"
msgstr ""

View File

@@ -9,8 +9,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-07-03 19:29+0700\n"
"PO-Revision-Date: 2025-07-03 19:28+0700\n"
"POT-Creation-Date: 2025-07-06 17:56+0500\n"
"PO-Revision-Date: 2025-07-06 17:56+0500\n"
"Last-Translator: \n"
"Language: ru_RU\n"
"Language-Team: ru_RU <LL@li.org>\n"
@@ -303,6 +303,12 @@ msgstr "Применить"
msgid "Invalid image"
msgstr "Недопустимое изображение"
msgid "Failed to download cover"
msgstr "Не удалось скачать обложку"
msgid "Downloading cover..."
msgstr "Скачивание обложки..."
msgid "No cover selected"
msgstr "Обложка не выбрана"
@@ -345,6 +351,9 @@ msgstr "Бронза"
msgid "Pending"
msgstr "В ожидании"
msgid "Unknown Game"
msgstr "Неизвестная игра"
msgid "Library"
msgstr "Библиотека"
@@ -369,9 +378,6 @@ msgstr "Загрузка игр из Steam..."
msgid "Loading PortProton games..."
msgstr "Загрузка игр из PortProton..."
msgid "Unknown Game"
msgstr "Неизвестная игра"
msgid "Game Library"
msgstr "Игровая библиотека"

View File

@@ -1,6 +1,7 @@
import gettext
from pathlib import Path
import locale
import os
from babel import Locale
LOCALE_MAP = {
@@ -72,3 +73,32 @@ def get_egs_language():
# Если что-то пошло не так — используем английский по умолчанию
return 'en'
def read_metadata_translations(metadata_file, language_code):
"""
Читает переводы из metadata.txt для указанного языка.
Возвращает словарь с полями name и description.
Для name: использует name_<language_code>, затем name_en, затем name, и наконец _('Unknown Game').
Для description: использует description_<language_code>, затем description_en, затем description.
"""
translations = {'name': _('Unknown Game'), 'description': ''}
if not os.path.exists(metadata_file):
return translations
with open(metadata_file, encoding='utf-8') as f:
for line in f:
line = line.strip()
if line.startswith(f'name_{language_code}='):
translations['name'] = line[len(f'name_{language_code}='):].strip()
elif line.startswith('name_en=') and translations['name'] == _('Unknown Game'):
translations['name'] = line[len('name_en='):].strip()
elif line.startswith('name=') and translations['name'] == _('Unknown Game'):
translations['name'] = line[len('name='):].strip()
elif line.startswith(f'description_{language_code}='):
translations['description'] = line[len(f'description_{language_code}='):].strip()
elif line.startswith('description_en=') and not translations['description']:
translations['description'] = line[len('description_en='):].strip()
elif line.startswith('description=') and not translations['description']:
translations['description'] = line[len('description='):].strip()
return translations

View File

@@ -28,7 +28,7 @@ from portprotonqt.config_utils import (
save_fullscreen_config, read_window_geometry, save_window_geometry, reset_config,
clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad, read_rumble_config, save_rumble_config
)
from portprotonqt.localization import _
from portprotonqt.localization import _, get_egs_language, read_metadata_translations
from portprotonqt.logger import get_logger
from portprotonqt.downloader import Downloader
@@ -465,11 +465,9 @@ class MainWindow(QMainWindow):
os.makedirs(user_custom_folder, exist_ok=True)
builtin_cover = ""
builtin_name = None
builtin_desc = None
user_cover = ""
user_name = None
user_desc = None
user_game_folder=""
builtin_game_folder=""
if game_exe:
exe_name = os.path.splitext(os.path.basename(game_exe))[0]
@@ -477,6 +475,7 @@ class MainWindow(QMainWindow):
user_game_folder = os.path.join(user_custom_folder, exe_name)
os.makedirs(user_game_folder, exist_ok=True)
# Чтение обложки
builtin_files = set(os.listdir(builtin_game_folder)) if os.path.exists(builtin_game_folder) else set()
for ext in [".jpg", ".png", ".jpeg", ".bmp"]:
candidate = f"cover{ext}"
@@ -484,16 +483,6 @@ class MainWindow(QMainWindow):
builtin_cover = os.path.join(builtin_game_folder, candidate)
break
builtin_metadata_file = os.path.join(builtin_game_folder, "metadata.txt")
if os.path.exists(builtin_metadata_file):
with open(builtin_metadata_file, encoding="utf-8") as f:
for line in f:
line = line.strip()
if line.startswith("name="):
builtin_name = line[len("name="):].strip()
elif line.startswith("description="):
builtin_desc = line[len("description="):].strip()
user_files = set(os.listdir(user_game_folder)) if os.path.exists(user_game_folder) else set()
for ext in [".jpg", ".png", ".jpeg", ".bmp"]:
candidate = f"cover{ext}"
@@ -501,16 +490,7 @@ class MainWindow(QMainWindow):
user_cover = os.path.join(user_game_folder, candidate)
break
user_metadata_file = os.path.join(user_game_folder, "metadata.txt")
if os.path.exists(user_metadata_file):
with open(user_metadata_file, encoding="utf-8") as f:
for line in f:
line = line.strip()
if line.startswith("name="):
user_name = line[len("name="):].strip()
elif line.startswith("description="):
user_desc = line[len("description="):].strip()
# Чтение статистики
if self.portproton_location:
statistics_file = os.path.join(self.portproton_location, "data", "tmp", "statistics")
try:
@@ -526,13 +506,26 @@ class MainWindow(QMainWindow):
print(f"Failed to parse playtime data: {e}")
def on_steam_info(steam_info: dict):
final_name = user_name or builtin_name or desktop_name
final_desc = (user_desc if user_desc is not None else
builtin_desc if builtin_desc is not None else
steam_info.get("description", ""))
# Определяем текущий язык
language_code = get_egs_language()
# Чтение переводов из metadata.txt
user_metadata_file = os.path.join(user_game_folder, "metadata.txt")
builtin_metadata_file = os.path.join(builtin_game_folder, "metadata.txt")
# Сначала пытаемся загрузить пользовательские переводы
translations = {'name': desktop_name, 'description': ''}
if os.path.exists(user_metadata_file):
translations = read_metadata_translations(user_metadata_file, language_code)
elif os.path.exists(builtin_metadata_file):
translations = read_metadata_translations(builtin_metadata_file, language_code)
final_name = translations['name']
final_desc = translations['description'] or steam_info.get("description", "")
final_cover = (user_cover if user_cover else
builtin_cover if builtin_cover else
steam_info.get("cover", "") or entry.get("Icon", ""))
callback((
final_name,
final_desc,

View File

@@ -44,7 +44,7 @@ dependencies = [
portprotonqt = "portprotonqt.app:main"
[tool.setuptools.package-data]
"portprotonqt" = ["themes/**/*", "locales/**/*", "custom_data/**/*"]
"portprotonqt" = ["themes/**/*", "locales/**/*"]
[tool.setuptools.packages.find]
exclude = ["build-aux", "dev-scripts", "documentation", "data"]