8 Commits

Author SHA1 Message Date
678f28ed30 bump ver to 0.1.10
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2026-01-14 17:37:42 +05:00
e7d2860c0e chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2026-01-14 17:36:58 +05:00
55a7f77a33 fix(detail_pages): handle RuntimeError on detail page exit animation
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2026-01-13 14:29:37 +05:00
8e6c0aafd1 fix(image_utils): remove corrupted cached images on load failure
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2026-01-13 14:21:11 +05:00
dd65021976 fix(time_utils): make playtime parsing robust to malformed data
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2026-01-13 14:13:59 +05:00
5f3a451c50 chore(locale): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2026-01-12 21:03:51 +05:00
c33813dae5 feat(get_wine): added total size to update_selection_display
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2026-01-12 20:56:41 +05:00
88a436c29f fix(locales): clean fuzzy in poedit
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2026-01-12 20:40:08 +05:00
21 changed files with 275 additions and 64 deletions

View File

@@ -8,7 +8,7 @@ on:
env:
# Common version, will be used for tagging the release
VERSION: 0.1.9
VERSION: 0.1.10
PKGDEST: "/tmp/portprotonqt"
PACKAGE: "portprotonqt"
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}

View File

@@ -3,6 +3,35 @@
Все заметные изменения в этом проекте фиксируются в этом файле.
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/).
## [0.1.10] - 2026-01-14
### Added
- Детальная страница для автоустановок с описанием игры и возможности переуствновки
- Менеджер версий Wine для скачивания и удаления различных версий Wine и Proton
- Возможность перевода описание, названия тем на другие языки
- Возможность перевода подписи к скриншотам тем на другие языки
### Changed
- Проведена чистка мёртвого кода
- Улучшена проверка сторонних тем
- В документации по созданию тем добавлены примеры dropin тем
- Провеедена редактура перевода
- Переработана сортировка вайнов и префиксов во всех комбобоксах
- Список Wine и префиксов теперь обновляется на лету, а не при запуске приложения
- AppImage теперь работает на дистрибутивах использующий альтернативный libc, а так же на тех что не следуют FHS
### Fixed
- Изменение размера карточек автоустановок через геймпад
- Проведены исправления для утечек памяти
- Время игры теперь парсится даже если файл статистики повреждён
- При наличии битых обложек они теперь перекачиваются, а не провоцируют ошибки libpng
- Управление QmessageBox через стрелки клавиатуры
### Contributors
- @Vector_null
- @Dervart
- @Simple16
## [0.1.9] - 2025-12-08
### Added

View File

@@ -1,5 +1,5 @@
pkgname=portprotonqt
pkgver=0.1.9
pkgver=0.1.10
pkgrel=1
pkgdesc="Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store"
arch=('any')

View File

@@ -1,5 +1,5 @@
%global pypi_name portprotonqt
%global pypi_version 0.1.9
%global pypi_version 0.1.10
%global oname PortProtonQt
%global _python_no_extras_requires 1

View File

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

View File

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

View File

@@ -556,8 +556,11 @@ class DetailPageAnimations:
if detail_page and not detail_page.isHidden():
detail_page.setGraphicsEffect(cast(Any, original_effect))
except RuntimeError:
logger.debug("Original effect already deleted")
cleanup_callback()
logger.debug("Detail page or effect already deleted")
try:
cleanup_callback()
except RuntimeError:
logger.debug("Error during cleanup callback")
# Check if animation is still valid before starting
if animation and not detail_page.isHidden():
@@ -594,10 +597,10 @@ class DetailPageAnimations:
animation.setEasingCurve(easing_curve)
def slide_cleanup():
# Check if page is still valid before cleanup
if not detail_page or detail_page.isHidden():
logger.debug("Detail page already cleaned up")
cleanup_callback()
try:
cleanup_callback()
except RuntimeError:
logger.debug("Error during slide cleanup callback")
# Check if animation is still valid before starting
if animation and not detail_page.isHidden():
@@ -647,20 +650,28 @@ class DetailPageAnimations:
return
def bounce_cleanup():
# Check if page is still valid before cleanup
if not detail_page or detail_page.isHidden():
logger.debug("Detail page already cleaned up")
cleanup_callback()
try:
cleanup_callback()
except RuntimeError:
logger.debug("Error during bounce cleanup callback")
group_anim.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
self.animations[detail_page] = group_anim
group_anim.finished.connect(bounce_cleanup)
except RuntimeError:
# Widget was already deleted, which is expected after deleteLater()
logger.debug("Detail page already deleted during animation setup")
cleanup_callback()
try:
cleanup_callback()
except RuntimeError:
pass
except Exception as e:
logger.error(f"Error in animate_detail_page_exit: {e}", exc_info=True)
if detail_page in self.animations:
self.animations.pop(detail_page, None)
cleanup_callback()
try:
if detail_page in self.animations:
self.animations.pop(detail_page, None)
except RuntimeError:
pass
try:
cleanup_callback()
except RuntimeError:
pass

View File

@@ -17,7 +17,7 @@ from portprotonqt.cli import parse_args
__app_id__ = "ru.linux_gaming.PortProtonQt"
__app_name__ = "PortProtonQt"
__app_version__ = "0.1.9"
__app_version__ = "0.1.10"
def get_version():
try:

View File

@@ -863,10 +863,12 @@ class DetailPageManager:
logger.warning("Detail page not valid, bypassing animation and cleaning up directly")
self._exit_animation_in_progress = False
cleanup()
except RuntimeError:
logger.debug("Page deleted before animation could start")
self._exit_animation_in_progress = False
except Exception as e:
logger.error(f"Error starting exit animation: {e}", exc_info=True)
self._exit_animation_in_progress = False
cleanup() # Fallback to cleanup if animation fails
def open_portproton_forum_topic(self, name):
result = self.portproton_api.get_forum_topic_slug(name)

View File

@@ -971,15 +971,69 @@ class ProtonManager(QDialog):
if os.path.exists(filepath):
total_size += os.path.getsize(filepath)
# Convert to human readable format
for unit in ['B', 'KB', 'MB', 'GB']:
if total_size < 1024.0:
return f"{total_size:.1f} {unit}"
total_size /= 1024.0
return f"{total_size:.1f} TB"
# Convert to human readable format (binary units)
if total_size == 0:
return "0 B"
elif total_size < 1024:
return f"{total_size}.0 B"
elif total_size < 1024 * 1024:
return f"{int(total_size / 1024)}.{int((total_size / 1024 * 10) % 10)} KiB"
elif total_size < 1024 * 1024 * 1024:
return f"{int(total_size / (1024 * 1024))}.{int((total_size / (1024 * 1024) * 10) % 10)} MiB"
elif total_size < 1024 * 1024 * 1024 * 1024:
return f"{int(total_size / (1024 * 1024 * 1024))}.{int((total_size / (1024 * 1024 * 1024) * 10) % 10)} GiB"
else:
return f"{int(total_size / (1024 * 1024 * 1024 * 1024))}.{int((total_size / (1024 * 1024 * 1024 * 1024) * 10) % 10)} TiB"
except Exception:
return _("Unknown")
def convert_size_to_bytes(self, size_str):
"""Convert human-readable size string to bytes"""
if not size_str or size_str == _("Unknown"):
return 0
# Remove any extra text and extract the number and unit
size_str = size_str.strip()
# Handle different units
if size_str.endswith("TiB"):
num = float(size_str[:-3].strip())
return int(num * 1024 * 1024 * 1024 * 1024)
elif size_str.endswith("GiB"):
num = float(size_str[:-3].strip())
return int(num * 1024 * 1024 * 1024)
elif size_str.endswith("MiB"):
num = float(size_str[:-3].strip())
return int(num * 1024 * 1024)
elif size_str.endswith("KiB"):
num = float(size_str[:-3].strip())
return int(num * 1024)
elif size_str.endswith("B"):
num = float(size_str[:-1].strip())
return int(num)
else:
# If format is unknown, return 0
return 0
def format_bytes(self, bytes_value):
"""Format bytes to human-readable string"""
if bytes_value == 0:
return "0 B"
elif bytes_value < 1024:
return f"{bytes_value} B"
elif bytes_value < 1024 * 1024:
kb_value = bytes_value / 1024
return f"{kb_value:.1f} KiB"
elif bytes_value < 1024 * 1024 * 1024:
mb_value = bytes_value / (1024 * 1024)
return f"{mb_value:.1f} MiB"
elif bytes_value < 1024 * 1024 * 1024 * 1024:
gb_value = bytes_value / (1024 * 1024 * 1024)
return f"{gb_value:.1f} GiB"
else:
tb_value = bytes_value / (1024 * 1024 * 1024 * 1024)
return f"{tb_value:.1f} TiB"
def on_cell_clicked(self, row):
"""Обработка клика по ячейке - переключение флажка при клике по любой ячейке в строке"""
tab = self.tab_widget.currentWidget()
@@ -1033,6 +1087,7 @@ class ProtonManager(QDialog):
table = current_tab.findChild(QTableWidget)
if table:
selected_count = 0
total_size = 0
for row in range(table.rowCount()):
checkbox_widget = table.cellWidget(row, 0)
@@ -1041,6 +1096,14 @@ class ProtonManager(QDialog):
if checkbox and checkbox.isChecked():
selected_count += 1
# Get the size for the selected item
size_item = table.item(row, 2) # Size column
if size_item:
size_text = size_item.text()
size_bytes = self.convert_size_to_bytes(size_text)
if size_bytes:
total_size += size_bytes
if selected_count > 0:
selection_text = _('Selected {} assets:\n').format(selected_count)
@@ -1061,6 +1124,10 @@ class ProtonManager(QDialog):
selection_text += f"{item_number}. {version_name}\n"
item_number += 1
# Add total size to the selection text
total_size_text = self.format_bytes(total_size)
selection_text += _("\nTotal size to delete: {}\n").format(total_size_text)
self.download_btn.setText(_('Delete Selected'))
self.download_btn.setEnabled(True)
else:
@@ -1078,9 +1145,49 @@ class ProtonManager(QDialog):
if self.selected_assets:
selection_text = _('Selected {} assets:\n').format(len(self.selected_assets))
total_size = 0
for i, asset_data in enumerate(self.selected_assets.values(), 1):
selection_text += f"{i}. {asset_data['asset_name']}\n"
# Get size from JSON entry if available
# We need to search through all tabs to find the matching entry
for tab_index in range(self.tab_widget.count()):
tab = self.tab_widget.widget(tab_index)
table = tab.findChild(QTableWidget)
if table and self.tab_widget.tabText(tab_index) != _("Installed"):
# Search for the item in the table to get its size
for row in range(table.rowCount()):
table_item = table.item(row, 1) # Name column
if table_item:
# Extract just the name without extensions for comparison
table_item_name = table_item.text()
# Remove common extensions for comparison
for ext in ['.tar.xz', '.tar.gz', '.zip']:
if table_item_name.lower().endswith(ext):
table_item_name = table_item_name[:-len(ext)]
break
asset_name_for_comparison = asset_data['asset_name']
for ext in ['.tar.xz', '.tar.gz', '.zip']:
if asset_name_for_comparison.lower().endswith(ext):
asset_name_for_comparison = asset_name_for_comparison[:-len(ext)]
break
if table_item_name == asset_name_for_comparison:
user_data = table_item.data(Qt.ItemDataRole.UserRole)
if user_data and 'json_entry' in user_data:
json_entry = user_data['json_entry']
size_text = json_entry.get('size_human', 'Unknown')
size_bytes = self.convert_size_to_bytes(size_text)
if size_bytes:
total_size += size_bytes
break
# Add total size to the selection text
total_size_text = self.format_bytes(total_size)
selection_text += _("\nTotal size to download: {}\n").format(total_size_text)
self.selection_text.setPlainText(selection_text)
self.download_btn.setText(_('Download Selected'))
self.download_btn.setEnabled(True)

View File

@@ -71,9 +71,11 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
pixmap = QPixmap(local_path)
# Check if the pixmap loaded successfully
if pixmap.isNull():
logger.warning(f"Failed to load image from {local_path}")
finish_with(pixmap)
return
logger.warning(f"Failed to load image from {local_path}, removing corrupted file")
os.remove(local_path)
else:
finish_with(pixmap)
return
def on_downloaded(result: str | None):
pixmap = QPixmap()
@@ -111,9 +113,11 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
pixmap = QPixmap(local_path)
# Check if the pixmap loaded successfully
if pixmap.isNull():
logger.warning(f"Failed to load image from {local_path}")
finish_with(pixmap)
return
logger.warning(f"Failed to load image from {local_path}, removing corrupted file")
os.remove(local_path)
else:
finish_with(pixmap)
return
def on_downloaded(result: str | None):
pixmap = QPixmap()
@@ -148,9 +152,11 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
pixmap = QPixmap(local_path)
# Check if the pixmap loaded successfully
if pixmap.isNull():
logger.warning(f"Failed to load image from {local_path}")
finish_with(pixmap)
return
logger.warning(f"Failed to load image from {local_path}, removing corrupted file")
os.remove(local_path)
else:
finish_with(pixmap)
return
def on_downloaded(result: str | None):
pixmap = QPixmap()
@@ -181,8 +187,13 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
# Check if the pixmap loaded successfully
if pixmap.isNull():
logger.warning(f"Failed to load image from {cover}")
finish_with(pixmap)
return
# Remove corrupted file only if it's in the cache directory
if cover.startswith(image_folder):
logger.warning(f"Removing corrupted cached file {cover}")
os.remove(cover)
else:
finish_with(pixmap)
return
placeholder_path = theme_manager.get_theme_image("placeholder", current_theme_name)
pixmap = QPixmap()

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-01-12 19:50+0500\n"
"POT-Creation-Date: 2026-01-12 20:59+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: de_DE\n"
@@ -544,9 +544,21 @@ msgstr ""
msgid "Selected {} assets:\n"
msgstr ""
#, python-brace-format
msgid ""
"\n"
"Total size to delete: {}\n"
msgstr ""
msgid "Delete Selected"
msgstr ""
#, python-brace-format
msgid ""
"\n"
"Total size to download: {}\n"
msgstr ""
msgid "Downloading in Progress"
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: 2026-01-12 19:50+0500\n"
"POT-Creation-Date: 2026-01-12 20:59+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: es_ES\n"
@@ -544,9 +544,21 @@ msgstr ""
msgid "Selected {} assets:\n"
msgstr ""
#, python-brace-format
msgid ""
"\n"
"Total size to delete: {}\n"
msgstr ""
msgid "Delete Selected"
msgstr ""
#, python-brace-format
msgid ""
"\n"
"Total size to download: {}\n"
msgstr ""
msgid "Downloading in Progress"
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: 2026-01-12 19:50+0500\n"
"POT-Creation-Date: 2026-01-12 20:59+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"
@@ -542,9 +542,21 @@ msgstr ""
msgid "Selected {} assets:\n"
msgstr ""
#, python-brace-format
msgid ""
"\n"
"Total size to delete: {}\n"
msgstr ""
msgid "Delete Selected"
msgstr ""
#, python-brace-format
msgid ""
"\n"
"Total size to download: {}\n"
msgstr ""
msgid "Downloading in Progress"
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: 2026-01-12 19:50+0500\n"
"PO-Revision-Date: 2026-01-03 20:32+0500\n"
"POT-Creation-Date: 2026-01-12 20:59+0500\n"
"PO-Revision-Date: 2026-01-12 20:59+0500\n"
"Last-Translator: \n"
"Language: ru_RU\n"
"Language-Team: ru_RU <LL@li.org>\n"
@@ -506,7 +506,6 @@ msgstr "Бронза"
msgid "Pending"
msgstr "В ожидании"
#, fuzzy
msgid "Manage Wine versions"
msgstr "Управление версиями WINE"
@@ -522,19 +521,16 @@ msgstr "Скачивание: "
msgid "Download Selected"
msgstr "Скачать выбранное"
#, fuzzy
msgid "Installed"
msgstr "Удаление WINE"
msgstr "Установленные"
#, fuzzy, python-brace-format
#, python-brace-format
msgid "Error loading wine data: {error}"
msgstr "Ошибка загрузки WINE: {error}"
#, fuzzy
msgid "Version Name"
msgstr "Версия WINE"
#, fuzzy
msgid "Size"
msgstr "Размер"
@@ -555,9 +551,25 @@ msgstr "Выберите, чтобы удалить WINE"
msgid "Selected {} assets:\n"
msgstr "Выбранно {} WINE:\n"
#, python-brace-format
msgid ""
"\n"
"Total size to delete: {}\n"
msgstr ""
"\n"
"Общий размер на удаление: {}\n"
msgid "Delete Selected"
msgstr "Удалить выбранное"
#, python-brace-format
msgid ""
"\n"
"Total size to download: {}\n"
msgstr ""
"\n"
"Общий размер на загрузку: {}\n"
msgid "Downloading in Progress"
msgstr "Скачивание"
@@ -573,27 +585,26 @@ msgstr "Пожалуйста выберите хотя бы один WINE для
msgid "Please wait for current downloading to complete."
msgstr "Пожалуйста подождите завершения скачивания."
#, fuzzy
msgid "Please select at least one version to delete."
msgstr "Пожалуйста выберите хотя бы один WINE для удаления."
#, fuzzy, python-brace-format
#, python-brace-format
msgid ""
"Are you sure you want to delete {} selected version(s)?\n"
"\n"
"This action cannot be undone."
msgstr ""
"Вы уверены, что хотите удалить выбранные WINE?\n"
"Вы уверены, что хотите удалить {} выбранные WINE?\n"
"\n"
"Это действие нельзя отменить."
#, fuzzy, python-brace-format
#, python-brace-format
msgid "Failed to remove version at {}: {}"
msgstr "Не удалось удалить WINE '{}': {}"
#, python-brace-format
msgid "Successfully removed {} version(s)."
msgstr "Успешно удалено {} WINE"
msgstr "Успешно удалено {} WINE."
msgid "Downloading Complete"
msgstr "Скачивание завершено"
@@ -738,7 +749,6 @@ msgstr "Удалить Префикс"
msgid "Clear Prefix"
msgstr "Очистить Префикс"
#, fuzzy
msgid "Manage WINE versions"
msgstr "Управление версиями WINE"

View File

@@ -94,8 +94,13 @@ def parse_playtime_file(file_path):
if len(parts) < 3:
continue
exe_path = parts[0]
seconds = int(parts[2])
playtime_data[exe_path] = seconds
# Find playtime: first numeric value after exe_path
# Format: <exe_path> <hash> <playtime_seconds> <platform> ...
# Hash is 64 hex chars, playtime is digits only
for i in range(1, len(parts)):
if parts[i].isdigit():
playtime_data[exe_path] = int(parts[i])
break
return playtime_data
def format_playtime(seconds):

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "portprotonqt"
version = "0.1.9"
version = "0.1.10"
description = "A project to rewrite PortProton (PortWINE) using PySide"
readme = "README.md"
license = { text = "GPL-3.0" }

2
uv.lock generated
View File

@@ -412,7 +412,7 @@ wheels = [
[[package]]
name = "portprotonqt"
version = "0.1.9"
version = "0.1.10"
source = { editable = "." }
dependencies = [
{ name = "babel" },