9 Commits
locale ... main

Author SHA1 Message Date
ae0b3a0f1a chore(build): added qt6-svg
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2026-01-14 18:08:32 +05:00
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
23 changed files with 279 additions and 66 deletions

View File

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

View File

@@ -3,6 +3,35 @@
Все заметные изменения в этом проекте фиксируются в этом файле. Все заметные изменения в этом проекте фиксируются в этом файле.
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/). Формат основан на [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 ## [0.1.9] - 2025-12-08
### Added ### Added

View File

@@ -1,12 +1,12 @@
pkgname=portprotonqt pkgname=portprotonqt
pkgver=0.1.9 pkgver=0.1.10
pkgrel=1 pkgrel=1
pkgdesc="Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store" pkgdesc="Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store"
arch=('any') arch=('any')
url="https://git.linux-gaming.ru/Boria138/PortProtonQt" url="https://git.linux-gaming.ru/Boria138/PortProtonQt"
license=('GPL-3.0') license=('GPL-3.0')
depends=('python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson' depends=('python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson'
'python-psutil' 'python-tqdm' 'python-vdf' 'python-libarchive-c' 'pyside6' 'python-rapidfuzz' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'unrar') 'python-psutil' 'python-tqdm' 'python-vdf' 'python-libarchive-c' 'pyside6' 'python-rapidfuzz' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'unrar' 'qt6-svg')
makedepends=('python-'{'build','installer','setuptools','wheel'}) makedepends=('python-'{'build','installer','setuptools','wheel'})
source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt#tag=v$pkgver") source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt#tag=v$pkgver")
sha256sums=('SKIP') sha256sums=('SKIP')

View File

@@ -6,7 +6,7 @@ arch=('any')
url="https://git.linux-gaming.ru/Boria138/PortProtonQt" url="https://git.linux-gaming.ru/Boria138/PortProtonQt"
license=('GPL-3.0') license=('GPL-3.0')
depends=('python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson' depends=('python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson'
'python-psutil' 'python-tqdm' 'python-vdf' 'python-libarchive-c' 'pyside6' 'icoextract' 'python-pillow' 'python-rapidfuzz' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'unrar') 'python-psutil' 'python-tqdm' 'python-vdf' 'python-libarchive-c' 'pyside6' 'icoextract' 'python-pillow' 'python-rapidfuzz' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'unrar' 'qt6-svg')
makedepends=('python-'{'build','installer','setuptools','wheel'}) makedepends=('python-'{'build','installer','setuptools','wheel'})
source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt.git") source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt.git")
sha256sums=('SKIP') sha256sums=('SKIP')

View File

@@ -48,6 +48,7 @@ Requires: python3-rapidfuzz
Requires: python3-libarchive-c Requires: python3-libarchive-c
Requires: perl-Image-ExifTool Requires: perl-Image-ExifTool
Requires: xdg-utils Requires: xdg-utils
Requires: qt6-qtsvg
Requires: cabextract Requires: cabextract
Requires: gzip Requires: gzip
Requires: unzip Requires: unzip

View File

@@ -1,5 +1,5 @@
%global pypi_name portprotonqt %global pypi_name portprotonqt
%global pypi_version 0.1.9 %global pypi_version 0.1.10
%global oname PortProtonQt %global oname PortProtonQt
%global _python_no_extras_requires 1 %global _python_no_extras_requires 1
@@ -45,6 +45,7 @@ Requires: python3-rapidfuzz
Requires: python3-libarchive-c Requires: python3-libarchive-c
Requires: perl-Image-ExifTool Requires: perl-Image-ExifTool
Requires: xdg-utils Requires: xdg-utils
Requires: qt6-qtsvg
Requires: cabextract Requires: cabextract
Requires: gzip Requires: gzip
Requires: unzip Requires: unzip

View File

@@ -21,9 +21,9 @@ Current translation status:
| Locale | Progress | Translated | | Locale | Progress | Translated |
| :----- | -------: | ---------: | | :----- | -------: | ---------: |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 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 374 | | [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 376 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 374 of 374 | | [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 | | [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 376 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 374 | | [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 376 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 374 из 374 | | [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(): if detail_page and not detail_page.isHidden():
detail_page.setGraphicsEffect(cast(Any, original_effect)) detail_page.setGraphicsEffect(cast(Any, original_effect))
except RuntimeError: except RuntimeError:
logger.debug("Original effect already deleted") logger.debug("Detail page or effect already deleted")
cleanup_callback() try:
cleanup_callback()
except RuntimeError:
logger.debug("Error during cleanup callback")
# Check if animation is still valid before starting # Check if animation is still valid before starting
if animation and not detail_page.isHidden(): if animation and not detail_page.isHidden():
@@ -594,10 +597,10 @@ class DetailPageAnimations:
animation.setEasingCurve(easing_curve) animation.setEasingCurve(easing_curve)
def slide_cleanup(): def slide_cleanup():
# Check if page is still valid before cleanup try:
if not detail_page or detail_page.isHidden(): cleanup_callback()
logger.debug("Detail page already cleaned up") except RuntimeError:
cleanup_callback() logger.debug("Error during slide cleanup callback")
# Check if animation is still valid before starting # Check if animation is still valid before starting
if animation and not detail_page.isHidden(): if animation and not detail_page.isHidden():
@@ -647,20 +650,28 @@ class DetailPageAnimations:
return return
def bounce_cleanup(): def bounce_cleanup():
# Check if page is still valid before cleanup try:
if not detail_page or detail_page.isHidden(): cleanup_callback()
logger.debug("Detail page already cleaned up") except RuntimeError:
cleanup_callback() logger.debug("Error during bounce cleanup callback")
group_anim.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped) group_anim.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
self.animations[detail_page] = group_anim self.animations[detail_page] = group_anim
group_anim.finished.connect(bounce_cleanup) group_anim.finished.connect(bounce_cleanup)
except RuntimeError: except RuntimeError:
# Widget was already deleted, which is expected after deleteLater()
logger.debug("Detail page already deleted during animation setup") logger.debug("Detail page already deleted during animation setup")
cleanup_callback() try:
cleanup_callback()
except RuntimeError:
pass
except Exception as e: except Exception as e:
logger.error(f"Error in animate_detail_page_exit: {e}", exc_info=True) logger.error(f"Error in animate_detail_page_exit: {e}", exc_info=True)
if detail_page in self.animations: try:
self.animations.pop(detail_page, None) if detail_page in self.animations:
cleanup_callback() 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_id__ = "ru.linux_gaming.PortProtonQt"
__app_name__ = "PortProtonQt" __app_name__ = "PortProtonQt"
__app_version__ = "0.1.9" __app_version__ = "0.1.10"
def get_version(): def get_version():
try: try:

View File

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

View File

@@ -971,15 +971,69 @@ class ProtonManager(QDialog):
if os.path.exists(filepath): if os.path.exists(filepath):
total_size += os.path.getsize(filepath) total_size += os.path.getsize(filepath)
# Convert to human readable format # Convert to human readable format (binary units)
for unit in ['B', 'KB', 'MB', 'GB']: if total_size == 0:
if total_size < 1024.0: return "0 B"
return f"{total_size:.1f} {unit}" elif total_size < 1024:
total_size /= 1024.0 return f"{total_size}.0 B"
return f"{total_size:.1f} TB" 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: except Exception:
return _("Unknown") 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): def on_cell_clicked(self, row):
"""Обработка клика по ячейке - переключение флажка при клике по любой ячейке в строке""" """Обработка клика по ячейке - переключение флажка при клике по любой ячейке в строке"""
tab = self.tab_widget.currentWidget() tab = self.tab_widget.currentWidget()
@@ -1033,6 +1087,7 @@ class ProtonManager(QDialog):
table = current_tab.findChild(QTableWidget) table = current_tab.findChild(QTableWidget)
if table: if table:
selected_count = 0 selected_count = 0
total_size = 0
for row in range(table.rowCount()): for row in range(table.rowCount()):
checkbox_widget = table.cellWidget(row, 0) checkbox_widget = table.cellWidget(row, 0)
@@ -1041,6 +1096,14 @@ class ProtonManager(QDialog):
if checkbox and checkbox.isChecked(): if checkbox and checkbox.isChecked():
selected_count += 1 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: if selected_count > 0:
selection_text = _('Selected {} assets:\n').format(selected_count) selection_text = _('Selected {} assets:\n').format(selected_count)
@@ -1061,6 +1124,10 @@ class ProtonManager(QDialog):
selection_text += f"{item_number}. {version_name}\n" selection_text += f"{item_number}. {version_name}\n"
item_number += 1 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.setText(_('Delete Selected'))
self.download_btn.setEnabled(True) self.download_btn.setEnabled(True)
else: else:
@@ -1078,9 +1145,49 @@ class ProtonManager(QDialog):
if self.selected_assets: if self.selected_assets:
selection_text = _('Selected {} assets:\n').format(len(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): for i, asset_data in enumerate(self.selected_assets.values(), 1):
selection_text += f"{i}. {asset_data['asset_name']}\n" 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.selection_text.setPlainText(selection_text)
self.download_btn.setText(_('Download Selected')) self.download_btn.setText(_('Download Selected'))
self.download_btn.setEnabled(True) 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) pixmap = QPixmap(local_path)
# Check if the pixmap loaded successfully # Check if the pixmap loaded successfully
if pixmap.isNull(): if pixmap.isNull():
logger.warning(f"Failed to load image from {local_path}") logger.warning(f"Failed to load image from {local_path}, removing corrupted file")
finish_with(pixmap) os.remove(local_path)
return else:
finish_with(pixmap)
return
def on_downloaded(result: str | None): def on_downloaded(result: str | None):
pixmap = QPixmap() pixmap = QPixmap()
@@ -111,9 +113,11 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
pixmap = QPixmap(local_path) pixmap = QPixmap(local_path)
# Check if the pixmap loaded successfully # Check if the pixmap loaded successfully
if pixmap.isNull(): if pixmap.isNull():
logger.warning(f"Failed to load image from {local_path}") logger.warning(f"Failed to load image from {local_path}, removing corrupted file")
finish_with(pixmap) os.remove(local_path)
return else:
finish_with(pixmap)
return
def on_downloaded(result: str | None): def on_downloaded(result: str | None):
pixmap = QPixmap() pixmap = QPixmap()
@@ -148,9 +152,11 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
pixmap = QPixmap(local_path) pixmap = QPixmap(local_path)
# Check if the pixmap loaded successfully # Check if the pixmap loaded successfully
if pixmap.isNull(): if pixmap.isNull():
logger.warning(f"Failed to load image from {local_path}") logger.warning(f"Failed to load image from {local_path}, removing corrupted file")
finish_with(pixmap) os.remove(local_path)
return else:
finish_with(pixmap)
return
def on_downloaded(result: str | None): def on_downloaded(result: str | None):
pixmap = QPixmap() 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 # Check if the pixmap loaded successfully
if pixmap.isNull(): if pixmap.isNull():
logger.warning(f"Failed to load image from {cover}") logger.warning(f"Failed to load image from {cover}")
finish_with(pixmap) # Remove corrupted file only if it's in the cache directory
return 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) placeholder_path = theme_manager.get_theme_image("placeholder", current_theme_name)
pixmap = QPixmap() pixmap = QPixmap()

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: de_DE\n" "Language: de_DE\n"
@@ -544,9 +544,21 @@ msgstr ""
msgid "Selected {} assets:\n" msgid "Selected {} assets:\n"
msgstr "" msgstr ""
#, python-brace-format
msgid ""
"\n"
"Total size to delete: {}\n"
msgstr ""
msgid "Delete Selected" msgid "Delete Selected"
msgstr "" msgstr ""
#, python-brace-format
msgid ""
"\n"
"Total size to download: {}\n"
msgstr ""
msgid "Downloading in Progress" msgid "Downloading in Progress"
msgstr "" msgstr ""

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: es_ES\n" "Language: es_ES\n"
@@ -544,9 +544,21 @@ msgstr ""
msgid "Selected {} assets:\n" msgid "Selected {} assets:\n"
msgstr "" msgstr ""
#, python-brace-format
msgid ""
"\n"
"Total size to delete: {}\n"
msgstr ""
msgid "Delete Selected" msgid "Delete Selected"
msgstr "" msgstr ""
#, python-brace-format
msgid ""
"\n"
"Total size to download: {}\n"
msgstr ""
msgid "Downloading in Progress" msgid "Downloading in Progress"
msgstr "" msgstr ""

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PortProtonQt 0.1.1\n" "Project-Id-Version: PortProtonQt 0.1.1\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -542,9 +542,21 @@ msgstr ""
msgid "Selected {} assets:\n" msgid "Selected {} assets:\n"
msgstr "" msgstr ""
#, python-brace-format
msgid ""
"\n"
"Total size to delete: {}\n"
msgstr ""
msgid "Delete Selected" msgid "Delete Selected"
msgstr "" msgstr ""
#, python-brace-format
msgid ""
"\n"
"Total size to download: {}\n"
msgstr ""
msgid "Downloading in Progress" msgid "Downloading in Progress"
msgstr "" msgstr ""

View File

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

View File

@@ -94,8 +94,13 @@ def parse_playtime_file(file_path):
if len(parts) < 3: if len(parts) < 3:
continue continue
exe_path = parts[0] exe_path = parts[0]
seconds = int(parts[2]) # Find playtime: first numeric value after exe_path
playtime_data[exe_path] = seconds # 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 return playtime_data
def format_playtime(seconds): def format_playtime(seconds):

View File

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

2
uv.lock generated
View File

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