Compare commits
	
		
			108 Commits
		
	
	
		
			ba9d8b76d8
			...
			main
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 0231073b19 | |||
| dec24429f5 | |||
| 4a758f3b3c | |||
| 0853dd1579 | |||
| bbb87c0455 | |||
| b32a71a125 | |||
|  | bddf9f850a | ||
|  | a9c3cfa167 | ||
| 7675bc4cdc | |||
| ffa203f019 | |||
| 3eed25ecee | |||
| 3736bb279e | |||
|  | b59ee5ae8e | ||
| 33176590fd | |||
| 8046065929 | |||
|  | fbad5add6c | ||
| 438e9737ea | |||
| 2d39a4c740 | |||
| 567203b0b0 | |||
| 502cbc5030 | |||
| 9b61215152 | |||
| 10d3fe8ab4 | |||
| a568ad9ef8 | |||
| f074843fc8 | |||
| 4ab078b93e | |||
| 7df6ad3b80 | |||
| 464ad0fe9c | |||
| cde92885d4 | |||
| 120c7b319c | |||
| 596aed0077 | |||
| 6fc6cb1e02 | |||
| 186e28a19b | |||
| 28e4d1e77c | |||
| fff1f888c4 | |||
| fdd5a0a3d5 | |||
| 792e52d981 | |||
| 84d5e46a74 | |||
| 4bc764d568 | |||
| 9a18aa037e | |||
| ed62d2d1c4 | |||
| accc9b18b6 | |||
| 82249d7eab | |||
| 476c896940 | |||
| b1047ba18e | |||
| 987199d8e6 | |||
|  | ef1acd4581 | ||
| 96f884904c | |||
| b856a2afae | |||
| 55ef0030e6 | |||
| 8aaeaa4824 | |||
| f55372b480 | |||
| 4d6f32f053 | |||
| a2f5141b20 | |||
| e3cb2857e7 | |||
| efe8a35832 | |||
| 61fae97dad | |||
| 5442100f64 | |||
| 2d6ef84798 | |||
|  | f4aee15b5d | ||
| 87a65108a5 | |||
| bb617708ac | |||
| 1cf332cd87 | |||
| 577ad4d3a3 | |||
| ef3f2d6e96 | |||
| 657d7728a6 | |||
| 9452bfda2e | |||
| 7eb2db0d68 | |||
| 6ef7a03366 | |||
| e5af354b56 | |||
| e6e5f6c8ea | |||
| 84306bb31b | |||
| 60af4d1482 | |||
| 692e11b21d | |||
| b1a804811e | |||
| 9a30cfaea7 | |||
| 5dd2f71f5e | |||
| dba172361b | |||
| a9c70b8818 | |||
| 135ace732f | |||
| 8b727f64e1 | |||
| a8eb591da5 | |||
| fe4ca1ee87 | |||
| ffe3e9d3d6 | |||
| 49d39b5d61 | |||
|  | 03566da704 | ||
|  | 7f996ab6a0 | ||
|  | 9e17978155 | ||
| 5d0185b1b4 | |||
| 5c134be04e | |||
| 8c66695192 | |||
| 7a141d8e46 | |||
| abb2377fb7 | |||
| 75f4f346de | |||
| 87a9f85272 | |||
| 240f685ece | |||
| af4e3e95bb | |||
| 017d9a42cf | |||
| 18b7c4054b | |||
| dd7f71b70a | |||
| 8fd44c575b | |||
| 65b43c1572 | |||
| f35276abfe | |||
| 6fea9a9a7e | |||
| 5189474631 | |||
|  | 416cc6a268 | ||
|  | 3b44ed5252 | ||
| c8c45dda06 | |||
| 3f9f794e6f | 
| @@ -94,7 +94,7 @@ jobs: | ||||
|     name: Build Arch Package | ||||
|     runs-on: ubuntu-22.04 | ||||
|     container: | ||||
|       image: archlinux:base-devel@sha256:0589aa8f31d8f64c630a2d1cc0b4c3847a1a63c988abd63d78d3c9bd94764f64 | ||||
|       image: archlinux:base-devel@sha256:87a967f07ba6319fc35c8c4e6ce6acdb4343b57aa817398a5d2db57bd8edc731 | ||||
|       volumes: | ||||
|         - /usr:/usr-host | ||||
|         - /opt:/opt-host | ||||
|   | ||||
| @@ -8,7 +8,7 @@ on: | ||||
|  | ||||
| env: | ||||
|   # Common version, will be used for tagging the release | ||||
|   VERSION: 0.1.6 | ||||
|   VERSION: 0.1.8 | ||||
|   PKGDEST: "/tmp/portprotonqt" | ||||
|   PACKAGE: "portprotonqt" | ||||
|   GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} | ||||
| @@ -180,10 +180,12 @@ jobs: | ||||
|  | ||||
|       - name: Release | ||||
|         uses: https://gitea.com/actions/gitea-release-action@v1 | ||||
|         env: | ||||
|             NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18 | ||||
|         with: | ||||
|           body_path: changelog.txt | ||||
|           token: ${{ env.GITEA_TOKEN }} | ||||
|           tag_name: v${{ env.VERSION }} | ||||
|           prerelease: true | ||||
|           files: release/**/* | ||||
|           sha256sum: true | ||||
|           sha256sum: false | ||||
|   | ||||
| @@ -138,7 +138,7 @@ jobs: | ||||
|     needs: changes | ||||
|     if: needs.changes.outputs.arch == 'true' || github.event_name == 'workflow_dispatch' | ||||
|     container: | ||||
|       image: archlinux:base-devel@sha256:0589aa8f31d8f64c630a2d1cc0b4c3847a1a63c988abd63d78d3c9bd94764f64 | ||||
|       image: archlinux:base-devel@sha256:87a967f07ba6319fc35c8c4e6ce6acdb4343b57aa817398a5d2db57bd8edc731 | ||||
|       volumes: | ||||
|         - /usr:/usr-host | ||||
|         - /opt:/opt-host | ||||
|   | ||||
| @@ -8,7 +8,7 @@ on: | ||||
| jobs: | ||||
|   renovate: | ||||
|     runs-on: ubuntu-latest | ||||
|     container: ghcr.io/renovatebot/renovate:latest@sha256:dd5721b9a686a40d81687643e4b71b82a0ca31fb653fd727538af69104fd388d | ||||
|     container: ghcr.io/renovatebot/renovate:latest@sha256:17c8966ef38fc361e108a550ffe2dcedf73e846f9975a974aea3d48c66b107a6 | ||||
|     steps: | ||||
|       - uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 | ||||
|  | ||||
|   | ||||
| @@ -11,12 +11,12 @@ repos: | ||||
|       - id: check-yaml | ||||
|  | ||||
|   - repo: https://github.com/astral-sh/uv-pre-commit | ||||
|     rev: 0.8.22 | ||||
|     rev: 0.9.5 | ||||
|     hooks: | ||||
|       - id: uv-lock | ||||
|  | ||||
|   - repo: https://github.com/astral-sh/ruff-pre-commit | ||||
|     rev: v0.13.2 | ||||
|     rev: v0.14.2 | ||||
|     hooks: | ||||
|       - id: ruff-check | ||||
|  | ||||
|   | ||||
							
								
								
									
										42
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						| @@ -3,20 +3,59 @@ | ||||
| Все заметные изменения в этом проекте фиксируются в этом файле. | ||||
| Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/). | ||||
|  | ||||
| ## [Unreleased] | ||||
| ## [0.1.8] - 2025-10-18 | ||||
|  | ||||
| ### Added | ||||
| - В настройки добавлен пункт для выбора типа геймпада для подсказок по управлению | ||||
| - В настройки добавлен пункт для выбора сворачивать ли приложение в трей или нет | ||||
| - К диалогу добавления игры, Winetricks, диалогу выбора файлов и виртуальной клавиатуре добавлены подсказки по управлению с геймпада | ||||
| - Во вкладку автоустановок добавлен слайдер изменения размера карточек (они со слайдером в библиотеке независимы) | ||||
|  | ||||
| ### Changed | ||||
| - При завершении автоустановки приложение больше не перезапускается | ||||
| - Выбор exe в диалоге добавления игры больше не перезаписывает введенное в поле название | ||||
| - Обновлены и дополнены скриншоты темы | ||||
|  | ||||
| ### Fixed | ||||
| - Исправлено наложение карточек при смене фильтра игр | ||||
| - Исправлена невозможность запуска приложения без подключёного геймпада | ||||
| - Исправлена невозможность установки компонентов Winetricks через геймпад | ||||
| - Ресиверы и виртуальные устройства больше не считаются за геймпад | ||||
|  | ||||
|  | ||||
| ### Contributors | ||||
| - @Vector_null | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## [0.1.7] - 2025-10-12 | ||||
|  | ||||
| ### Added | ||||
| - Возможность скроллинга библиотеки мышью или пальцем | ||||
| - Импорт и экспорт бекапа префикса | ||||
| - Диалог для управление Winetricks | ||||
| - Кнопки для удаления префикса, wine или proton | ||||
| - Все настройки Wine с оригинального PortProton | ||||
| - Виртуальная клавиатура в диалог добавления игры и поиск по библиотеке и автоустановках | ||||
| - Вкладка автоустановок | ||||
| - В заголовке окна теперь отображается версия приложения и хеш коммита если запуск идёт с гита | ||||
|  | ||||
| ### Changed | ||||
| - Проведён рефакторинг и оптимизация всего что связано с карточками и библиотекой игр | ||||
| - В диалог выбора файлов в режиме directory_only (при выборе куда сохранить бекап префикса) добавлена кнопка ./ обозначающая нынешнюю папку | ||||
|  | ||||
| ### Fixed | ||||
| - Исправлен вылет диалога выбора файлов при выборе обложки если в папке более сотни изображений | ||||
| - Исправлено зависание при добавлении или удалении игры в Wayland | ||||
| - Исправлено зависание при поиске игр | ||||
| - Исправлено ошибочное присвоение ID игры с названием "GAME", возникавшее, если исполняемый файл находился в подпапке `game/` (часто встречается у игр на Unity) | ||||
| - Исправлена ошибка из-за которой подсказки по управлению снизу и сверху могли не совпадать с друг другом, из-за чего возле вкладок были стрелки клавиатуры, а снизу кнопки геймпада | ||||
| - Исправлен выход из полноэкранного режима при отключении геймпада подключённого по USB даже если настройка "Режим полноэкранного отображения приложения при подключении геймпада" выключена | ||||
| - При сохранении настроек теперь не меняется размер окна | ||||
|  | ||||
| ### Contributors | ||||
| - @wmigor (Igor Akulov) | ||||
| - @Vector_null | ||||
|  | ||||
| --- | ||||
|  | ||||
| @@ -39,6 +78,7 @@ | ||||
|  | ||||
| ### Contributors | ||||
| - @wmigor (Igor Akulov) | ||||
| - @Vector_null | ||||
|  | ||||
| --- | ||||
|  | ||||
|   | ||||
| @@ -54,8 +54,6 @@ PortProtonQt использует код и зависимости от след | ||||
| - [Legendary](https://github.com/derrod/legendary) — инструмент для работы с Epic Games Store, лицензия [GPL-3.0](https://github.com/derrod/legendary/blob/master/LICENSE). | ||||
| - [Icoextract](https://github.com/jlu5/icoextract) — библиотека для извлечения иконок, лицензия [MIT](https://github.com/jlu5/icoextract/blob/master/LICENSE). | ||||
| - [HowLongToBeat Python API](https://github.com/ScrappyCocco/HowLongToBeat-PythonAPI) — библиотека для взаимодействия с HowLongToBeat, лицензия [MIT](https://github.com/ScrappyCocco/HowLongToBeat-PythonAPI/blob/master/LICENSE.md). | ||||
| - [Those Awesome Guys: Gamepad prompts images](https://thoseawesomeguys.com/prompts/) - Набор подсказок для геймпада и клавиатур, лицензия [CC0](https://creativecommons.org/public-domain/cc0/) | ||||
|  | ||||
| Полный текст лицензий см. в файле [LICENSE](LICENSE). | ||||
|  | ||||
| > [!WARNING] | ||||
|   | ||||
							
								
								
									
										15
									
								
								TODO.md
									
									
									
									
									
								
							
							
						
						| @@ -1,6 +1,6 @@ | ||||
| - [X] Адаптировать структуру проекта для поддержки инструментов сборки | ||||
| - [X] Добавить возможность управления с геймпада | ||||
| - [ ] Добавить возможность управления с тачскрина | ||||
| - [X] Добавить возможность управления с тачскрина (Формально и так есть) | ||||
| - [X] Добавить возможность управления с мыши и клавиатуры | ||||
| - [X] Добавить систему тем [Документация](documentation/theme_guide) | ||||
| - [X] Вынести все константы, такие как уровень закругления карточек, в темы (частично выполнено) | ||||
| @@ -11,18 +11,18 @@ | ||||
| - [ ] Разработать адаптивный дизайн (за эталон берётся Steam Deck с разрешением 1280×800) | ||||
| - [ ] Переделать скриншоты для соответствия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots) | ||||
| - [X] Получать описания и названия игр из базы данных Steam | ||||
| - [X] Получать обложки для игр из SteamGridDB или CDN Steam | ||||
| - [X] Получать обложки для игр из CDN Steam | ||||
| - [X] Оптимизировать работу со Steam API для ускорения времени запуска | ||||
| - [X] Улучшить функцию поиска в Steam API для исправления некорректного определения ID (например, Graven определялся как ENGRAVEN или GRAVENFALL, Spore — как SporeBound или Spore Valley) | ||||
| - [ ] Убрать логи Steam API в релизной версии, так как они замедляют выполнение кода | ||||
| - [X] Убрать логи Steam API в релизной версии, так как они замедляют выполнение кода | ||||
| - [X] Решить проблему с ограничением Steam API в 50 тысяч игр за один запрос (иногда нужные игры не попадают в выборку и остаются без обложки) | ||||
| - [X] Избавиться от вызовов yad | ||||
| - [X] Реализовать собственный системный трей вместо использования трея PortProton | ||||
| - [X] Добавить экранную клавиатуру в поиск (реализация собственной клавиатуры слишком затратна, поэтому используется встроенная в DE клавиатура: Maliit в KDE, gjs-osk в GNOME, Squeekboard в Phosh, клавиатура SteamOS и т.д.) | ||||
| - [X] Добавить экранную клавиатуру в поиск | ||||
| - [X] Добавить сортировку карточек по различным критериям (доступны: по недавности, количеству наигранного времени, избранному или алфавиту) | ||||
| - [X] Добавить индикацию запуска приложения | ||||
| - [X] Достигнуть паритета функциональности с Ingame | ||||
| - [ ] Достигнуть паритета функциональности с PortProton | ||||
| - [ ] Достигнуть паритета функциональности с PortProton (остались настройки игр и обновление скриптов) | ||||
| - [X] Добавить возможность изменения названия, описания и обложки через файлы `.local/share/PortProtonQT/custom_data/exe_name/{desc,name,cover}` | ||||
| - [X] Добавить встроенное переопределение названия, описания и обложки, например, по пути `portprotonqt/custom_data` [Документация](documentation/metadata_override/) | ||||
| - [X] Добавить переводы в переопределения | ||||
| @@ -49,7 +49,7 @@ | ||||
| - [X] Добавить недокументированные параметры конфигурации в GUI (time_detail_level, games_sort_method, games_display_filter) | ||||
| - [X] Добавить систему избранного для карточек | ||||
| - [X] Заменить все `print` на `logging` | ||||
| - [ ] Привести все логи к единому языку | ||||
| - [X] Привести все логи к единому языку | ||||
| - [X] Уменьшить количество подстановок в переводах | ||||
| - [X] Стилизовать все элементы без стилей (QMessageBox, QSlider, QDialog) | ||||
| - [X] Убрать жёсткую привязку путей к стрелочкам QComboBox в `styles.py` | ||||
| @@ -62,7 +62,6 @@ | ||||
| - [X] Исправить некорректную работу слайдера увеличения размера карточек([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63) | ||||
| - [X] Исправить баг с наложением карточек друг на друга при изменении фильтра отображения ([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63)) | ||||
| - [X] Скопировать логику управления с D-pad на стрелки с клавиатуры | ||||
| - [ ] Доделать светлую тему | ||||
| - [ ] Добавить подсказки к управлению с геймпада | ||||
| - [X] Добавить подсказки к управлению с геймпада | ||||
| - [X] Добавить миниатюры к выбору файлов в диалоге добавления игры | ||||
| - [X] Добавить быстрый доступ к смонтированным дискам к выбору файлов в диалоге добавления игры | ||||
|   | ||||
| @@ -36,7 +36,7 @@ AppDir: | ||||
|     id: ru.linux_gaming.PortProtonQt | ||||
|     name: PortProtonQt | ||||
|     icon: ru.linux_gaming.PortProtonQt | ||||
|     version: 0.1.6 | ||||
|     version: 0.1.8 | ||||
|     exec: usr/bin/python3 | ||||
|     exec_args: "-m portprotonqt.app $@" | ||||
|   apt: | ||||
| @@ -54,6 +54,11 @@ AppDir: | ||||
|       - libxcb-cursor0 | ||||
|       - libimage-exiftool-perl | ||||
|       - xdg-utils | ||||
|       - cabextract | ||||
|       - curl | ||||
|       - 7zip | ||||
|       - unzip | ||||
|       - unrar | ||||
|     exclude: | ||||
|       - "*-doc" | ||||
|       - "*-man" | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| pkgname=portprotonqt | ||||
| pkgver=0.1.6 | ||||
| pkgver=0.1.8 | ||||
| pkgrel=1 | ||||
| pkgdesc="Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store" | ||||
| arch=('any') | ||||
| url="https://git.linux-gaming.ru/Boria138/PortProtonQt" | ||||
| license=('GPL-3.0') | ||||
| depends=('python-numpy' 'python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson' | ||||
|     'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client') | ||||
|     'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'unrar') | ||||
| makedepends=('python-'{'build','installer','setuptools','wheel'}) | ||||
| source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt#tag=v$pkgver") | ||||
| sha256sums=('SKIP') | ||||
|   | ||||
| @@ -6,7 +6,7 @@ arch=('any') | ||||
| url="https://git.linux-gaming.ru/Boria138/PortProtonQt" | ||||
| license=('GPL-3.0') | ||||
| depends=('python-numpy' 'python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson' | ||||
|     'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client') | ||||
|     'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'unrar') | ||||
| makedepends=('python-'{'build','installer','setuptools','wheel'}) | ||||
| source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt.git") | ||||
| sha256sums=('SKIP') | ||||
|   | ||||
| @@ -46,6 +46,11 @@ Requires:       python3-pillow | ||||
| Requires:       perl-Image-ExifTool | ||||
| Requires:       xdg-utils | ||||
| Requires:       python3-beautifulsoup4 | ||||
| Requires:       cabextract | ||||
| Requires:       gzip | ||||
| Requires:       unzip | ||||
| Requires:       curl | ||||
| Requires:       unrar | ||||
|  | ||||
| %description -n python3-%{pypi_name}-git | ||||
| This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup. | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| %global pypi_name portprotonqt | ||||
| %global pypi_version 0.1.6 | ||||
| %global pypi_version 0.1.8 | ||||
| %global oname PortProtonQt | ||||
| %global _python_no_extras_requires 1 | ||||
|  | ||||
| @@ -43,6 +43,11 @@ Requires:       python3-pillow | ||||
| Requires:       perl-Image-ExifTool | ||||
| Requires:       xdg-utils | ||||
| Requires:       python3-beautifulsoup4 | ||||
| Requires:       cabextract | ||||
| Requires:       gzip | ||||
| Requires:       unzip | ||||
| Requires:       curl | ||||
| Requires:       unrar | ||||
|  | ||||
| %description -n python3-%{pypi_name} | ||||
| This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup. | ||||
|   | ||||
| @@ -21,9 +21,9 @@ Current translation status: | ||||
|  | ||||
| | Locale | Progress | Translated | | ||||
| | :----- | -------: | ---------: | | ||||
| | [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 193 | | ||||
| | [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 193 | | ||||
| | [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 193 of 193 | | ||||
| | [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 249 | | ||||
| | [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 249 | | ||||
| | [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 249 of 249 | | ||||
|  | ||||
| --- | ||||
|  | ||||
|   | ||||
| @@ -21,9 +21,9 @@ | ||||
|  | ||||
| | Локаль | Прогресс | Переведено | | ||||
| | :----- | -------: | ---------: | | ||||
| | [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 193 | | ||||
| | [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 193 | | ||||
| | [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 193 из 193 | | ||||
| | [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 249 | | ||||
| | [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 249 | | ||||
| | [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 249 из 249 | | ||||
|  | ||||
| --- | ||||
|  | ||||
|   | ||||
| @@ -1,17 +1,46 @@ | ||||
| import sys | ||||
| from PySide6.QtCore import QLocale, QTranslator, QLibraryInfo | ||||
| import os | ||||
| import subprocess | ||||
| from PySide6.QtCore import QLocale, QTranslator, QLibraryInfo, QTimer, Qt | ||||
| from PySide6.QtWidgets import QApplication | ||||
| from PySide6.QtGui import QIcon | ||||
| from PySide6.QtNetwork import QLocalServer, QLocalSocket | ||||
|  | ||||
| from portprotonqt.main_window import MainWindow | ||||
| from portprotonqt.config_utils import save_fullscreen_config | ||||
| from portprotonqt.config_utils import ( | ||||
|     save_fullscreen_config, | ||||
|     read_fullscreen_config, | ||||
|     get_portproton_start_command | ||||
| ) | ||||
| from portprotonqt.logger import get_logger, setup_logger | ||||
| from portprotonqt.cli import parse_args | ||||
|  | ||||
| __app_id__ = "ru.linux_gaming.PortProtonQt" | ||||
| __app_name__ = "PortProtonQt" | ||||
| __app_version__ = "0.1.6" | ||||
| __app_version__ = "0.1.8" | ||||
|  | ||||
| def get_version(): | ||||
|     try: | ||||
|         commit = subprocess.check_output( | ||||
|             ["git", "rev-parse", "--short", "HEAD"], | ||||
|             stderr=subprocess.DEVNULL, | ||||
|         ).decode("utf-8").strip() | ||||
|         return f"{__app_version__} ({commit})" | ||||
|     except (subprocess.CalledProcessError, FileNotFoundError, OSError): | ||||
|         return __app_version__ | ||||
|  | ||||
| def main(): | ||||
|     os.environ["PW_CLI"] = "1" | ||||
|     os.environ["PROCESS_LOG"] = "1" | ||||
|     os.environ["START_FROM_STEAM"] = "1" | ||||
|  | ||||
|     start_sh = get_portproton_start_command() | ||||
|  | ||||
|     if start_sh is None: | ||||
|         return | ||||
|  | ||||
|     subprocess.run(start_sh + ["cli", "--initial"]) | ||||
|  | ||||
|     app = QApplication(sys.argv) | ||||
|     app.setWindowIcon(QIcon.fromTheme(__app_id__)) | ||||
|     app.setDesktopFileName(__app_id__) | ||||
| @@ -19,40 +48,116 @@ def main(): | ||||
|     app.setApplicationVersion(__app_version__) | ||||
|  | ||||
|     args = parse_args() | ||||
|  | ||||
|     # Setup logger with specified debug level | ||||
|     setup_logger(args.debug_level) | ||||
|  | ||||
|     # Reinitialize logger after setup to ensure it uses the new configuration | ||||
|     logger = get_logger(__name__) | ||||
|  | ||||
|     # --- Single-instance logic --- | ||||
|     server_name = __app_id__ | ||||
|     socket = QLocalSocket() | ||||
|     socket.connectToServer(server_name) | ||||
|  | ||||
|     if socket.waitForConnected(200): | ||||
|         # Второй экземпляр — передаём команду первому | ||||
|         fullscreen = args.fullscreen or read_fullscreen_config() | ||||
|         msg = b"show:fullscreen" if fullscreen else b"show" | ||||
|         socket.write(msg) | ||||
|         socket.flush() | ||||
|         socket.waitForBytesWritten(500) | ||||
|         socket.disconnectFromServer() | ||||
|         logger.info("Restored existing instance from tray") | ||||
|         return | ||||
|  | ||||
|     # Если старый сокет остался — удалить | ||||
|     QLocalServer.removeServer(server_name) | ||||
|  | ||||
|     local_server = QLocalServer() | ||||
|     if not local_server.listen(server_name): | ||||
|         logger.warning(f"Failed to start local server: {local_server.errorString()}") | ||||
|         return | ||||
|  | ||||
|     # --- Qt translations --- | ||||
|     system_locale = QLocale.system() | ||||
|     qt_translator = QTranslator() | ||||
|     translations_path = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath) | ||||
|     if qt_translator.load(system_locale, "qtbase", "_", translations_path): | ||||
|         app.installTranslator(qt_translator) | ||||
|     else: | ||||
|         logger.warning(f"Qt translations for {system_locale.name()} not found in {translations_path}, using english language") | ||||
|         logger.warning( | ||||
|             f"Qt translations for {system_locale.name()} not found in {translations_path}, using English" | ||||
|         ) | ||||
|  | ||||
|     window = MainWindow(app_name=__app_name__) | ||||
|     # --- Main Window --- | ||||
|     version = get_version() | ||||
|     window = MainWindow(app_name=__app_name__, version=version) | ||||
|  | ||||
|     if args.fullscreen: | ||||
|         logger.info("Launching in fullscreen mode due to --fullscreen flag") | ||||
|     # --- Handle incoming connections --- | ||||
|     def handle_new_connection(): | ||||
|         conn = local_server.nextPendingConnection() | ||||
|         if not conn: | ||||
|             return | ||||
|  | ||||
|         if conn.waitForReadyRead(1000): | ||||
|             data = conn.readAll().data() | ||||
|             msg = bytes(data).decode("utf-8", errors="ignore") | ||||
|             logger.info(f"IPC message received: {msg}") | ||||
|  | ||||
|             def restore_window(): | ||||
|                 try: | ||||
|                     if msg.startswith("show"): | ||||
|                         if hasattr(window, "restore_from_tray"): | ||||
|                             window.restore_from_tray()  # type: ignore[attr-defined] | ||||
|                         else: | ||||
|                             window.showNormal() | ||||
|                             window.raise_() | ||||
|                             window.activateWindow() | ||||
|                             window.setWindowState( | ||||
|                                 window.windowState() & ~Qt.WindowState.WindowMinimized | Qt.WindowState.WindowActive | ||||
|                             ) | ||||
|  | ||||
|                         if ":fullscreen" in msg: | ||||
|                             logger.info("Switching to fullscreen via IPC") | ||||
|                             save_fullscreen_config(True) | ||||
|                             window.showFullScreen() | ||||
|                         else: | ||||
|                             logger.info("Switching to normal window via IPC") | ||||
|                             save_fullscreen_config(False) | ||||
|                             window.showNormal() | ||||
|                 except Exception as e: | ||||
|                     logger.warning(f"Failed to restore window: {e}") | ||||
|  | ||||
|             # Выполняем в основном потоке | ||||
|             QTimer.singleShot(0, restore_window) | ||||
|  | ||||
|         conn.disconnectFromServer() | ||||
|  | ||||
|     local_server.newConnection.connect(handle_new_connection) | ||||
|  | ||||
|     # --- Initial fullscreen state --- | ||||
|     launch_fullscreen = args.fullscreen or read_fullscreen_config() | ||||
|     if launch_fullscreen: | ||||
|         logger.info( | ||||
|             f"Launching in fullscreen mode ({'--fullscreen' if args.fullscreen else 'config'})" | ||||
|         ) | ||||
|         save_fullscreen_config(True) | ||||
|         window.showFullScreen() | ||||
|     else: | ||||
|         logger.info("Launching in normal mode") | ||||
|         save_fullscreen_config(False) | ||||
|         window.showNormal() | ||||
|  | ||||
|     # --- Cleanup --- | ||||
|     def cleanup_on_exit(): | ||||
|         nonlocal window | ||||
|         app.aboutToQuit.disconnect() | ||||
|         if window: | ||||
|             window.close() | ||||
|         app.quit() | ||||
|         try: | ||||
|             local_server.close() | ||||
|             QLocalServer.removeServer(server_name) | ||||
|             if window: | ||||
|                 window.close() | ||||
|         except Exception as e: | ||||
|             logger.warning(f"Cleanup error: {e}") | ||||
|  | ||||
|     app.aboutToQuit.connect(cleanup_on_exit) | ||||
|  | ||||
|     window.show() | ||||
|  | ||||
|     sys.exit(app.exec()) | ||||
|  | ||||
| if __name__ == '__main__': | ||||
| if __name__ == "__main__": | ||||
|     main() | ||||
|   | ||||
| @@ -1,11 +1,13 @@ | ||||
| import os | ||||
| import configparser | ||||
| import shutil | ||||
| import subprocess | ||||
| from portprotonqt.logger import get_logger | ||||
|  | ||||
| logger = get_logger(__name__) | ||||
|  | ||||
| _portproton_location = None | ||||
| _portproton_start_sh = None | ||||
|  | ||||
| # Paths to configuration files | ||||
| CONFIG_FILE = os.path.join( | ||||
| @@ -101,14 +103,14 @@ def read_file_content(file_path): | ||||
|         return f.read().strip() | ||||
|  | ||||
| def get_portproton_location(): | ||||
|     """Returns the path to the PortProton directory. | ||||
|     Checks the cached path first. If not set, reads from PORTPROTON_CONFIG_FILE. | ||||
|     If the path is invalid, uses the default directory. | ||||
|     """ | ||||
|     """Возвращает путь к PortProton каталогу (строку) или None.""" | ||||
|     global _portproton_location | ||||
|  | ||||
|     if _portproton_location is not None: | ||||
|         return _portproton_location | ||||
|  | ||||
|     location = None | ||||
|  | ||||
|     if os.path.isfile(PORTPROTON_CONFIG_FILE): | ||||
|         try: | ||||
|             location = read_file_content(PORTPROTON_CONFIG_FILE).strip() | ||||
| @@ -116,19 +118,46 @@ def get_portproton_location(): | ||||
|                 _portproton_location = location | ||||
|                 logger.info(f"PortProton path from configuration: {location}") | ||||
|                 return _portproton_location | ||||
|             logger.warning(f"Invalid PortProton path in configuration: {location}, using default path") | ||||
|             logger.warning(f"Invalid PortProton path in configuration: {location}, using defaults") | ||||
|         except (OSError, PermissionError) as e: | ||||
|             logger.warning(f"Failed to read PortProton configuration file: {e}, using default path") | ||||
|             logger.warning(f"Failed to read PortProton configuration file: {e}") | ||||
|  | ||||
|     default_dir = os.path.join(os.path.expanduser("~"), ".var", "app", "ru.linux_gaming.PortProton") | ||||
|     if os.path.isdir(default_dir): | ||||
|         _portproton_location = default_dir | ||||
|         logger.info(f"Using flatpak PortProton directory: {default_dir}") | ||||
|     default_flatpak_dir = os.path.join(os.path.expanduser("~"), ".var", "app", "ru.linux_gaming.PortProton") | ||||
|     if os.path.isdir(default_flatpak_dir): | ||||
|         _portproton_location = default_flatpak_dir | ||||
|         logger.info(f"Using Flatpak PortProton directory: {default_flatpak_dir}") | ||||
|         return _portproton_location | ||||
|  | ||||
|     logger.warning("PortProton configuration and flatpak directory not found") | ||||
|     logger.warning("PortProton configuration and Flatpak directory not found") | ||||
|     return None | ||||
|  | ||||
| def get_portproton_start_command(): | ||||
|     """Возвращает список команд для запуска PortProton (start.sh или flatpak run).""" | ||||
|     portproton_path = get_portproton_location() | ||||
|     if not portproton_path: | ||||
|         return None | ||||
|  | ||||
|     try: | ||||
|         result = subprocess.run( | ||||
|             ["flatpak", "list"], | ||||
|             capture_output=True, | ||||
|             text=True, | ||||
|             check=False | ||||
|         ) | ||||
|         if "ru.linux_gaming.PortProton" in result.stdout: | ||||
|             logger.info("Detected Flatpak installation") | ||||
|             return ["flatpak", "run", "ru.linux_gaming.PortProton"] | ||||
|     except Exception: | ||||
|         pass | ||||
|  | ||||
|     start_sh_path = os.path.join(portproton_path, "data", "scripts", "start.sh") | ||||
|     if os.path.exists(start_sh_path): | ||||
|         return [start_sh_path] | ||||
|  | ||||
|     logger.warning("Neither flatpak nor start.sh found for PortProton") | ||||
|     return None | ||||
|  | ||||
|  | ||||
| def parse_desktop_entry(file_path): | ||||
|     """Reads and parses a .desktop file using configparser. | ||||
|     Returns None if the [Desktop Entry] section is missing. | ||||
| @@ -177,6 +206,26 @@ def save_card_size(card_width): | ||||
|     with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: | ||||
|         cp.write(configfile) | ||||
|  | ||||
| def read_auto_card_size(): | ||||
|     """Reads the card size (width) for Auto Install from the [Cards] section. | ||||
|     Returns 250 if the parameter is not set. | ||||
|     """ | ||||
|     cp = read_config_safely(CONFIG_FILE) | ||||
|     if cp is None or not cp.has_section("Cards") or not cp.has_option("Cards", "auto_card_width"): | ||||
|         save_auto_card_size(250) | ||||
|         return 250 | ||||
|     return cp.getint("Cards", "auto_card_width", fallback=250) | ||||
|  | ||||
| def save_auto_card_size(card_width): | ||||
|     """Saves the card size (width) for Auto Install to the [Cards] section.""" | ||||
|     cp = read_config_safely(CONFIG_FILE) or configparser.ConfigParser() | ||||
|     if "Cards" not in cp: | ||||
|         cp["Cards"] = {} | ||||
|     cp["Cards"]["auto_card_width"] = str(card_width) | ||||
|     with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: | ||||
|         cp.write(configfile) | ||||
|  | ||||
|  | ||||
| def read_sort_method(): | ||||
|     """Reads the sort method from the [Games] section. | ||||
|     Returns 'last_launch' if the parameter is not set. | ||||
| @@ -259,6 +308,25 @@ def save_rumble_config(rumble_enabled): | ||||
|     with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: | ||||
|         cp.write(configfile) | ||||
|  | ||||
| def read_gamepad_type(): | ||||
|     """Reads the gamepad type from the [Gamepad] section. | ||||
|     Returns 'xbox' if the parameter is missing. | ||||
|     """ | ||||
|     cp = read_config_safely(CONFIG_FILE) | ||||
|     if cp is None or not cp.has_section("Gamepad") or not cp.has_option("Gamepad", "type"): | ||||
|         save_gamepad_type("xbox") | ||||
|         return "xbox" | ||||
|     return cp.get("Gamepad", "type", fallback="xbox").lower() | ||||
|  | ||||
| def save_gamepad_type(gpad_type): | ||||
|     """Saves the gamepad type to the [Gamepad] section.""" | ||||
|     cp = read_config_safely(CONFIG_FILE) or configparser.ConfigParser() | ||||
|     if "Gamepad" not in cp: | ||||
|         cp["Gamepad"] = {} | ||||
|     cp["Gamepad"]["type"] = gpad_type | ||||
|     with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: | ||||
|         cp.write(configfile) | ||||
|  | ||||
| def ensure_default_proxy_config(): | ||||
|     """Ensures the [Proxy] section exists in the configuration file. | ||||
|     Creates it with empty values if missing. | ||||
| @@ -408,3 +476,22 @@ def save_favorite_folders(folders): | ||||
|     cp["FavoritesFolders"]["folders"] = f'"{fav_str}"' | ||||
|     with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: | ||||
|         cp.write(configfile) | ||||
|  | ||||
| def read_minimize_to_tray(): | ||||
|     """Reads the minimize-to-tray setting from the [Display] section. | ||||
|     Returns True if the parameter is missing (default: minimize to tray). | ||||
|     """ | ||||
|     cp = read_config_safely(CONFIG_FILE) | ||||
|     if cp is None or not cp.has_section("Display") or not cp.has_option("Display", "minimize_to_tray"): | ||||
|         save_minimize_to_tray(True) | ||||
|         return True | ||||
|     return cp.getboolean("Display", "minimize_to_tray", fallback=True) | ||||
|  | ||||
| def save_minimize_to_tray(minimize_to_tray): | ||||
|     """Saves the minimize-to-tray setting to the [Display] section.""" | ||||
|     cp = read_config_safely(CONFIG_FILE) or configparser.ConfigParser() | ||||
|     if "Display" not in cp: | ||||
|         cp["Display"] = {} | ||||
|     cp["Display"]["minimize_to_tray"] = str(minimize_to_tray) | ||||
|     with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: | ||||
|         cp.write(configfile) | ||||
|   | ||||
| @@ -11,7 +11,7 @@ from PySide6.QtWidgets import QMessageBox, QDialog, QMenu, QLineEdit, QApplicati | ||||
| from PySide6.QtCore import QUrl, QPoint, QObject, Signal, Qt | ||||
| from PySide6.QtGui import QDesktopServices, QIcon, QKeySequence | ||||
| from portprotonqt.localization import _ | ||||
| from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites, read_favorite_folders, save_favorite_folders | ||||
| from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites, read_favorite_folders, save_favorite_folders, get_portproton_start_command | ||||
| from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam | ||||
| from portprotonqt.egs_api import add_egs_to_steam, get_egs_executable, remove_egs_from_steam | ||||
| from portprotonqt.dialogs import AddGameDialog, FileExplorer, generate_thumbnail | ||||
| @@ -406,16 +406,7 @@ class ContextMenuManager: | ||||
|                 ) | ||||
|                 return | ||||
|             # Construct EGS launch command | ||||
|             wrapper = "flatpak run ru.linux_gaming.PortProton" | ||||
|             start_sh_path = os.path.join(self.portproton_location, "data", "scripts", "start.sh") | ||||
|             if self.portproton_location and ".var" not in self.portproton_location: | ||||
|                 wrapper = start_sh_path | ||||
|                 if not os.path.exists(start_sh_path): | ||||
|                     self.signals.show_warning_dialog.emit( | ||||
|                         _("Error"), | ||||
|                         _("start.sh not found at {path}").format(path=start_sh_path) | ||||
|                     ) | ||||
|                     return | ||||
|             wrapper = get_portproton_start_command() | ||||
|             exec_line = f'"{self.legendary_path}" launch {game_card.appid} --no-wine --wrapper "env START_FROM_STEAM=1 {wrapper}"' | ||||
|         else: | ||||
|             exec_line = self._get_exec_line(game_card.name, game_card.exec_line) | ||||
|   | ||||
| Before Width: | Height: | Size: 634 KiB After Width: | Height: | Size: 634 KiB | 
| Before Width: | Height: | Size: 315 KiB After Width: | Height: | Size: 315 KiB | 
| Before Width: | Height: | Size: 978 KiB After Width: | Height: | Size: 978 KiB | 
| Before Width: | Height: | Size: 650 KiB After Width: | Height: | Size: 650 KiB | 
| Before Width: | Height: | Size: 391 KiB After Width: | Height: | Size: 391 KiB | 
| Before Width: | Height: | Size: 710 KiB After Width: | Height: | Size: 710 KiB | 
| Before Width: | Height: | Size: 627 KiB After Width: | Height: | Size: 627 KiB | 
| @@ -126,7 +126,21 @@ class FlowLayout(QLayout): | ||||
|         return True | ||||
|  | ||||
|     def heightForWidth(self, width): | ||||
|         return self.doLayout(QRect(0, 0, width, 0), True) | ||||
|         # Аналогично фильтруем видимые для тестового расчёта высоты | ||||
|         visible_items = [] | ||||
|         nat_sizes = np.empty((0, 2), dtype=np.int32) | ||||
|         for item in self.itemList: | ||||
|             if item.widget() and item.widget().isVisible(): | ||||
|                 visible_items.append(item) | ||||
|                 s = item.sizeHint() | ||||
|                 new_row = np.array([[s.width(), s.height()]], dtype=np.int32) | ||||
|                 nat_sizes = np.vstack([nat_sizes, new_row]) if len(nat_sizes) > 0 else new_row | ||||
|  | ||||
|         if len(visible_items) == 0: | ||||
|             return 0 | ||||
|  | ||||
|         _, total_height = compute_layout(nat_sizes, width, self._spacing, self._max_scale) | ||||
|         return total_height | ||||
|  | ||||
|     def setGeometry(self, rect): | ||||
|         super().setGeometry(rect) | ||||
| @@ -145,26 +159,46 @@ class FlowLayout(QLayout): | ||||
|         return size | ||||
|  | ||||
|     def doLayout(self, rect, testOnly): | ||||
|         N = len(self.itemList) | ||||
|         if N == 0: | ||||
|         N_total = len(self.itemList) | ||||
|         if N_total == 0: | ||||
|             return 0 | ||||
|  | ||||
|         nat_sizes = np.empty((N, 2), dtype=np.int32) | ||||
|         # Фильтруем только видимые элементы | ||||
|         visible_items = [] | ||||
|         visible_indices = []  # Индексы в оригинальном itemList для установки геометрии | ||||
|         nat_sizes = np.empty((0, 2), dtype=np.int32) | ||||
|         for i, item in enumerate(self.itemList): | ||||
|             s = item.sizeHint() | ||||
|             nat_sizes[i, 0] = s.width() | ||||
|             nat_sizes[i, 1] = s.height() | ||||
|             if item.widget() and item.widget().isVisible(): | ||||
|                 visible_items.append(item) | ||||
|                 visible_indices.append(i) | ||||
|                 s = item.sizeHint() | ||||
|                 new_row = np.array([[s.width(), s.height()]], dtype=np.int32) | ||||
|                 nat_sizes = np.vstack([nat_sizes, new_row]) if len(nat_sizes) > 0 else new_row | ||||
|  | ||||
|         N = len(visible_items) | ||||
|         if N == 0: | ||||
|             # Если все скрыты, устанавливаем нулевые геометрии для всех | ||||
|             if not testOnly: | ||||
|                 for item in self.itemList: | ||||
|                     item.setGeometry(QRect()) | ||||
|             return 0 | ||||
|  | ||||
|         geom_array, total_height = compute_layout(nat_sizes, rect.width(), self._spacing, self._max_scale) | ||||
|  | ||||
|         if not testOnly: | ||||
|             for i, item in enumerate(self.itemList): | ||||
|                 x = geom_array[i, 0] + rect.x() | ||||
|                 y = geom_array[i, 1] + rect.y() | ||||
|                 w = geom_array[i, 2] | ||||
|                 h = geom_array[i, 3] | ||||
|             # Устанавливаем геометрии только для видимых | ||||
|             for idx, (_vis_idx, item) in enumerate(zip(visible_indices, visible_items, strict=True)): | ||||
|                 x = geom_array[idx, 0] + rect.x() | ||||
|                 y = geom_array[idx, 1] + rect.y() | ||||
|                 w = geom_array[idx, 2] | ||||
|                 h = geom_array[idx, 3] | ||||
|                 item.setGeometry(QRect(QPoint(x, y), QSize(w, h))) | ||||
|  | ||||
|             # Для невидимых — нулевая геометрия | ||||
|             for i in range(N_total): | ||||
|                 if i not in visible_indices: | ||||
|                     self.itemList[i].setGeometry(QRect()) | ||||
|  | ||||
|         return total_height | ||||
|  | ||||
| class ClickableLabel(QLabel): | ||||
|   | ||||
| @@ -13,7 +13,7 @@ from portprotonqt.localization import get_egs_language, _ | ||||
| from portprotonqt.logger import get_logger | ||||
| from portprotonqt.image_utils import load_pixmap_async | ||||
| from portprotonqt.time_utils import parse_playtime_file, format_playtime, get_last_launch, get_last_launch_timestamp | ||||
| from portprotonqt.config_utils import get_portproton_location | ||||
| from portprotonqt.config_utils import get_portproton_location, get_portproton_start_command | ||||
| from portprotonqt.steam_api import ( | ||||
|     get_weanticheatyet_status_async, get_steam_apps_and_index_async, get_protondb_tier_async, | ||||
|     search_app, get_steam_home, get_last_steam_user, convert_steam_id, generate_thumbnail, call_steam_api | ||||
| @@ -254,14 +254,7 @@ def add_egs_to_steam(app_name: str, game_title: str, legendary_path: str, callba | ||||
|         return | ||||
|  | ||||
|     # Determine wrapper | ||||
|     wrapper = "flatpak run ru.linux_gaming.PortProton" | ||||
|     start_sh_path = os.path.join(portproton_dir, "data", "scripts", "start.sh") | ||||
|     if portproton_dir is not None and ".var" not in portproton_dir: | ||||
|         wrapper = start_sh_path | ||||
|         if not os.path.exists(start_sh_path): | ||||
|             logger.error(f"start.sh not found at {start_sh_path}") | ||||
|             callback((False, f"start.sh not found at {start_sh_path}")) | ||||
|             return | ||||
|     wrapper = get_portproton_start_command() | ||||
|  | ||||
|     # Create launch script | ||||
|     steam_scripts_dir = os.path.join(portproton_dir, "steam_scripts") | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| from PySide6.QtGui import QPainter, QColor, QDesktopServices | ||||
| from PySide6.QtCore import Signal, Property, Qt, QUrl | ||||
| from PySide6.QtCore import Signal, Property, Qt, QUrl, QTimer | ||||
| from PySide6.QtWidgets import QFrame, QGraphicsDropShadowEffect, QVBoxLayout, QWidget, QStackedLayout, QLabel | ||||
| from collections.abc import Callable | ||||
| from portprotonqt.image_utils import load_pixmap_async, round_corners | ||||
| @@ -12,6 +12,7 @@ from portprotonqt.downloader import Downloader | ||||
| from portprotonqt.animations import GameCardAnimations | ||||
| from typing import cast | ||||
|  | ||||
|  | ||||
| class GameCard(QFrame): | ||||
|     borderWidthChanged = Signal() | ||||
|     gradientAngleChanged = Signal() | ||||
| @@ -403,6 +404,13 @@ class GameCard(QFrame): | ||||
|             self.favoriteLabel.setText("☆") | ||||
|         self.favoriteLabel.setStyleSheet(self.theme.FAVORITE_LABEL_STYLE) | ||||
|  | ||||
|         parent = self.parent() | ||||
|         while parent: | ||||
|             if hasattr(parent, 'game_library_manager'): | ||||
|                 QTimer.singleShot(0, parent.game_library_manager.update_game_grid) # type: ignore[attr-defined] | ||||
|                 break | ||||
|             parent = parent.parent() | ||||
|  | ||||
|     def toggle_favorite(self): | ||||
|         favorites = read_favorites() | ||||
|         if self.is_favorite: | ||||
| @@ -447,6 +455,7 @@ class GameCard(QFrame): | ||||
|     gradientAngle = Property(float, getGradientAngle, setGradientAngle, None, "", notify=cast(Callable[[], None], gradientAngleChanged)) | ||||
|     scale = Property(float, getScale, setScale, None, "", notify=cast(Callable[[], None], scaleChanged)) | ||||
|  | ||||
|  | ||||
|     def paintEvent(self, event): | ||||
|         super().paintEvent(event) | ||||
|         self.animations.paint_border(QPainter(self)) | ||||
|   | ||||
| @@ -33,8 +33,10 @@ class MainWindowProtocol(Protocol): | ||||
|     # Required attributes | ||||
|     searchEdit: CustomLineEdit | ||||
|     _last_card_width: int | ||||
|     card_width: int | ||||
|     current_hovered_card: GameCard | None | ||||
|     current_focused_card: GameCard | None | ||||
|     gamesListWidget: QWidget | None | ||||
|  | ||||
| class GameLibraryManager: | ||||
|     def __init__(self, main_window: MainWindowProtocol, theme, context_menu_manager: ContextMenuManager | None): | ||||
| @@ -127,6 +129,8 @@ class GameLibraryManager: | ||||
|         self.card_width = self.sizeSlider.value() | ||||
|         self.sizeSlider.setToolTip(f"{self.card_width} px") | ||||
|         save_card_size(self.card_width) | ||||
|         self.main_window.card_width = self.card_width | ||||
|         self.main_window._last_card_width = self.card_width | ||||
|         for card in self.game_card_cache.values(): | ||||
|             card.update_card_size(self.card_width) | ||||
|         self.update_game_grid() | ||||
| @@ -216,6 +220,16 @@ class GameLibraryManager: | ||||
|         else: | ||||
|             self._update_game_grid_immediate() | ||||
|  | ||||
|     def force_update_cards_library(self): | ||||
|         if self.gamesListWidget and self.gamesListLayout: | ||||
|             self.gamesListLayout.invalidate() | ||||
|             self.gamesListWidget.updateGeometry() | ||||
|             widget = self.gamesListWidget | ||||
|             QTimer.singleShot(0, lambda: ( | ||||
|                 widget.adjustSize(), | ||||
|                 widget.updateGeometry() | ||||
|             )) | ||||
|  | ||||
|     def _update_game_grid_immediate(self): | ||||
|         """Updates the game grid with the provided or current game list.""" | ||||
|         if self.gamesListLayout is None or self.gamesListWidget is None: | ||||
| @@ -345,6 +359,8 @@ class GameLibraryManager: | ||||
|                 self.gamesListWidget.updateGeometry() | ||||
|                 self.main_window._last_card_width = self.card_width | ||||
|  | ||||
|                 self.force_update_cards_library() | ||||
|  | ||||
|         self.is_filtering = False  # Reset flag in any case | ||||
|  | ||||
|     def _apply_filter_visibility(self, search_text: str): | ||||
| @@ -362,8 +378,9 @@ class GameLibraryManager: | ||||
|                     cover_path, width, height, callback = self.pending_images.pop(game_key) | ||||
|                     load_pixmap_async(cover_path, width, height, callback) | ||||
|  | ||||
|         # Force geometry update so FlowLayout accounts for hidden widgets | ||||
|         # Force full relayout after visibility changes | ||||
|         if self.gamesListLayout is not None: | ||||
|             self.gamesListLayout.invalidate()  # Принудительно инвалидируем для пересчёта | ||||
|             self.gamesListLayout.update() | ||||
|         if self.gamesListWidget is not None: | ||||
|             self.gamesListWidget.updateGeometry() | ||||
|   | ||||
| @@ -83,6 +83,43 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q | ||||
|             except Exception as e: | ||||
|                 logger.error(f"Ошибка обработки URL {cover}: {e}") | ||||
|  | ||||
|         # SteamGridDB (SGDB) | ||||
|         if cover and cover.startswith("https://cdn2.steamgriddb.com"): | ||||
|             try: | ||||
|                 parts = cover.split("/") | ||||
|                 filename = parts[-1] if parts else "sgdb_cover.png" | ||||
|                 # SGDB ссылки содержат уникальный хеш в названии — используем как имя | ||||
|                 local_path = os.path.join(image_folder, filename) | ||||
|  | ||||
|                 if os.path.exists(local_path): | ||||
|                     pixmap = QPixmap(local_path) | ||||
|                     finish_with(pixmap) | ||||
|                     return | ||||
|  | ||||
|                 def on_downloaded(result: str | None): | ||||
|                     pixmap = QPixmap() | ||||
|                     if result and os.path.exists(result): | ||||
|                         pixmap.load(result) | ||||
|                     if pixmap.isNull(): | ||||
|                         placeholder_path = theme_manager.get_theme_image("placeholder", current_theme_name) | ||||
|                         if placeholder_path and QFile.exists(placeholder_path): | ||||
|                             pixmap.load(placeholder_path) | ||||
|                         else: | ||||
|                             pixmap = QPixmap(width, height) | ||||
|                             pixmap.fill(QColor("#333333")) | ||||
|                             painter = QPainter(pixmap) | ||||
|                             painter.setPen(QPen(QColor("white"))) | ||||
|                             painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "No Image") | ||||
|                             painter.end() | ||||
|                     finish_with(pixmap) | ||||
|  | ||||
|                 logger.info("Downloading SGDB cover for %s -> %s", app_name or "unknown", filename) | ||||
|                 downloader.download_async(cover, local_path, timeout=5, callback=on_downloaded) | ||||
|                 return | ||||
|  | ||||
|             except Exception as e: | ||||
|                 logger.error(f"Ошибка обработки SGDB URL {cover}: {e}") | ||||
|  | ||||
|         if cover and cover.startswith(("http://", "https://")): | ||||
|             try: | ||||
|                 local_path = os.path.join(image_folder, f"{app_name}.jpg") | ||||
|   | ||||
							
								
								
									
										73
									
								
								portprotonqt/keyboard_layouts.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,73 @@ | ||||
| # keyboard_layouts.py | ||||
| keyboard_layouts = { | ||||
|     'en': { | ||||
|         'normal': [ | ||||
|             ['`', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '='], | ||||
|             ['TAB', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\\'], | ||||
|             ['CAPS', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', "'"], | ||||
|             ['⬆', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/'] | ||||
|         ], | ||||
|         'shift': [ | ||||
|             ['~', '!', '@', '#', '$', '%', '^', '&&', '*', '(', ')', '_', '+'], | ||||
|             ['TAB', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', '|'], | ||||
|             ['CAPS', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '"'], | ||||
|             ['⬆', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?'] | ||||
|         ] | ||||
|     }, | ||||
|     'ru': { | ||||
|         'normal': [ | ||||
|             ['ё', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '='], | ||||
|             ['TAB', 'й', 'ц', 'у', 'к', 'е', 'н', 'г', 'ш', 'щ', 'з', 'х', 'ъ', '\\'], | ||||
|             ['CAPS', 'ф', 'ы', 'в', 'а', 'п', 'р', 'о', 'л', 'д', 'ж', 'э'], | ||||
|             ['⬆', 'я', 'ч', 'с', 'м', 'и', 'т', 'ь', 'б', 'ю', '.'] | ||||
|         ], | ||||
|         'shift': [ | ||||
|             ['Ё', '!', '"', '№', ';', '%', ':', '?', '*', '(', ')', '_', '+'], | ||||
|             ['TAB', 'Й', 'Ц', 'У', 'К', 'Е', 'Н', 'Г', 'Ш', 'Щ', 'З', 'Х', 'Ъ', '/'], | ||||
|             ['CAPS', 'Ф', 'Ы', 'В', 'А', 'П', 'Р', 'О', 'Л', 'Д', 'Ж', 'Э'], | ||||
|             ['⬆', 'Я', 'Ч', 'С', 'М', 'И', 'Т', 'Ь', 'Б', 'Ю', ','] | ||||
|         ] | ||||
|     }, | ||||
|     'fr': { | ||||
|         'normal': [ | ||||
|             ['²', '&', 'é', '"', "'", '(', '-', 'è', '_', 'ç', 'à', ')', '='], | ||||
|             ['TAB', 'a', 'z', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '^', '$', '*'], | ||||
|             ['CAPS', 'q', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'ù'], | ||||
|             ['⬆', 'w', 'x', 'c', 'v', 'b', 'n', ',', ';', ':', '!'] | ||||
|         ], | ||||
|         'shift': [ | ||||
|             ['', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '°', '+'], | ||||
|             ['TAB', 'A', 'Z', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '¨', '£', 'µ'], | ||||
|             ['CAPS', 'Q', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M', '%'], | ||||
|             ['⬆', 'W', 'X', 'C', 'V', 'B', 'N', '?', '.', '/', '§'] | ||||
|         ] | ||||
|     }, | ||||
|     'es': { | ||||
|         'normal': [ | ||||
|             ['º', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', "'", '¡'], | ||||
|             ['TAB', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '`', '+', '\\'], | ||||
|             ['CAPS', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'ñ', "'", 'ç'], | ||||
|             ['⬆', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '-'] | ||||
|         ], | ||||
|         'shift': [ | ||||
|             ['ª', '!', '"', '·', '$', '%', '&', '/', '(', ')', '=', '?', '¿'], | ||||
|             ['TAB', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '^', '*', '|'], | ||||
|             ['CAPS', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Ñ', '"', 'Ç'], | ||||
|             ['⬆', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ';', ':', '_'] | ||||
|         ] | ||||
|     }, | ||||
|     'de': { | ||||
|         'normal': [ | ||||
|             ['^', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'ß', '´'], | ||||
|             ['TAB', 'q', 'w', 'e', 'r', 't', 'z', 'u', 'i', 'o', 'p', 'ü', '+', '#'], | ||||
|             ['CAPS', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'ö', 'ä'], | ||||
|             ['⬆', '<', 'y', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '-'] | ||||
|         ], | ||||
|         'shift': [ | ||||
|             ['°', '!', '"', '§', '$', '%', '&', '/', '(', ')', '=', '?', '`'], | ||||
|             ['TAB', 'Q', 'W', 'E', 'R', 'T', 'Z', 'U', 'I', 'O', 'P', 'Ü', '*', '\''], | ||||
|             ['CAPS', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Ö', 'Ä'], | ||||
|             ['⬆', '>', 'Y', 'X', 'C', 'V', 'B', 'N', 'M', ';', ':', '_'] | ||||
|         ] | ||||
|     } | ||||
| } | ||||
| @@ -9,7 +9,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PROJECT VERSION\n" | ||||
| "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" | ||||
| "POT-Creation-Date: 2025-09-23 22:23+0500\n" | ||||
| "POT-Creation-Date: 2025-10-16 14:54+0500\n" | ||||
| "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | ||||
| "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | ||||
| "Language: de_DE\n" | ||||
| @@ -191,6 +191,10 @@ msgstr "" | ||||
| msgid "Failed to delete custom data: {error}" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Added '{game_name}' successfully" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Game name and executable path are required" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -248,13 +252,37 @@ msgstr "" | ||||
| msgid "Select All" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Launching {0}" | ||||
| msgid "Open" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Select Dir" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prev Dir" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Cancel" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Toggle" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Install" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Force Install" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prev Tab" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Next Tab" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Launching {0}" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "File Explorer" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -304,6 +332,39 @@ msgstr "" | ||||
| msgid "No cover selected" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prefix Manager" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Set" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Libraries" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Information" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Fonts" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Settings" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Winetricks not found. Please try again." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Warning" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "No components selected." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Installation failed. Check logs." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Components installed successfully." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Loading Epic Games Store games..." | ||||
| msgstr "" | ||||
|  | ||||
| @@ -352,9 +413,6 @@ msgstr "" | ||||
| msgid "Auto Install" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Emulators" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Wine Settings" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -370,6 +428,28 @@ msgstr "" | ||||
| msgid "Fullscreen" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Search" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Installation already in progress." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Failed to start installation." | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Processed {} installation..." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Installation completed successfully." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Installation failed." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Installation error." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Loading Steam games..." | ||||
| msgstr "" | ||||
|  | ||||
| @@ -382,13 +462,106 @@ msgstr "" | ||||
| msgid "Find Games ..." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Here you can configure automatic game installation..." | ||||
| #, python-brace-format | ||||
| msgid "Added '{name}'" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "List of available emulators and their configuration..." | ||||
| msgid "Compatibility tool:" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Various Wine parameters and versions..." | ||||
| msgid "Prefix:" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Wine Configuration" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Registry Editor" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Command Prompt" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Uninstaller" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Create Prefix Backup" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Load Prefix Backup" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Delete Compatibility Tool" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Delete Prefix" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Clear Prefix" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Launching tool..." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Failed to start process." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Confirm Clear" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Are you sure you want to clear prefix '{}'?" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Prefix '{}' cleared successfully." | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "" | ||||
| "Prefix '{}' cleared with errors:\n" | ||||
| "{}" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Failed to start backup process." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Failed to start restore process." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prefix backup completed." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prefix backup failed." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prefix restore completed." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prefix restore failed." | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Are you sure you want to delete prefix '{}'?" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Prefix '{}' deleted." | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Failed to delete prefix: {}" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Are you sure you want to delete compatibility tool '{}'?" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Compatibility tool '{}' deleted." | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Failed to delete compatibility tool: {}" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Main PortProton parameters..." | ||||
| @@ -424,6 +597,9 @@ msgstr "" | ||||
| msgid "Games Display Filter:" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Gamepad Type:" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Proxy URL" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -448,6 +624,12 @@ msgstr "" | ||||
| msgid "Application Fullscreen Mode:" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Minimize to tray on close" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Application Close Mode:" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Auto Fullscreen on Gamepad connected" | ||||
| msgstr "" | ||||
|  | ||||
|   | ||||
| @@ -9,7 +9,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PROJECT VERSION\n" | ||||
| "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" | ||||
| "POT-Creation-Date: 2025-09-23 22:23+0500\n" | ||||
| "POT-Creation-Date: 2025-10-16 14:54+0500\n" | ||||
| "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | ||||
| "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | ||||
| "Language: es_ES\n" | ||||
| @@ -191,6 +191,10 @@ msgstr "" | ||||
| msgid "Failed to delete custom data: {error}" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Added '{game_name}' successfully" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Game name and executable path are required" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -248,13 +252,37 @@ msgstr "" | ||||
| msgid "Select All" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Launching {0}" | ||||
| msgid "Open" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Select Dir" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prev Dir" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Cancel" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Toggle" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Install" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Force Install" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prev Tab" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Next Tab" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Launching {0}" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "File Explorer" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -304,6 +332,39 @@ msgstr "" | ||||
| msgid "No cover selected" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prefix Manager" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Set" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Libraries" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Information" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Fonts" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Settings" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Winetricks not found. Please try again." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Warning" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "No components selected." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Installation failed. Check logs." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Components installed successfully." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Loading Epic Games Store games..." | ||||
| msgstr "" | ||||
|  | ||||
| @@ -352,9 +413,6 @@ msgstr "" | ||||
| msgid "Auto Install" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Emulators" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Wine Settings" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -370,6 +428,28 @@ msgstr "" | ||||
| msgid "Fullscreen" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Search" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Installation already in progress." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Failed to start installation." | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Processed {} installation..." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Installation completed successfully." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Installation failed." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Installation error." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Loading Steam games..." | ||||
| msgstr "" | ||||
|  | ||||
| @@ -382,13 +462,106 @@ msgstr "" | ||||
| msgid "Find Games ..." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Here you can configure automatic game installation..." | ||||
| #, python-brace-format | ||||
| msgid "Added '{name}'" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "List of available emulators and their configuration..." | ||||
| msgid "Compatibility tool:" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Various Wine parameters and versions..." | ||||
| msgid "Prefix:" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Wine Configuration" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Registry Editor" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Command Prompt" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Uninstaller" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Create Prefix Backup" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Load Prefix Backup" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Delete Compatibility Tool" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Delete Prefix" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Clear Prefix" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Launching tool..." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Failed to start process." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Confirm Clear" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Are you sure you want to clear prefix '{}'?" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Prefix '{}' cleared successfully." | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "" | ||||
| "Prefix '{}' cleared with errors:\n" | ||||
| "{}" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Failed to start backup process." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Failed to start restore process." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prefix backup completed." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prefix backup failed." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prefix restore completed." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prefix restore failed." | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Are you sure you want to delete prefix '{}'?" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Prefix '{}' deleted." | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Failed to delete prefix: {}" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Are you sure you want to delete compatibility tool '{}'?" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Compatibility tool '{}' deleted." | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Failed to delete compatibility tool: {}" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Main PortProton parameters..." | ||||
| @@ -424,6 +597,9 @@ msgstr "" | ||||
| msgid "Games Display Filter:" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Gamepad Type:" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Proxy URL" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -448,6 +624,12 @@ msgstr "" | ||||
| msgid "Application Fullscreen Mode:" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Minimize to tray on close" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Application Close Mode:" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Auto Fullscreen on Gamepad connected" | ||||
| msgstr "" | ||||
|  | ||||
|   | ||||
| @@ -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-09-23 22:23+0500\n" | ||||
| "POT-Creation-Date: 2025-10-16 14:54+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" | ||||
| @@ -189,6 +189,10 @@ msgstr "" | ||||
| msgid "Failed to delete custom data: {error}" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Added '{game_name}' successfully" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Game name and executable path are required" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -246,13 +250,37 @@ msgstr "" | ||||
| msgid "Select All" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Launching {0}" | ||||
| msgid "Open" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Select Dir" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prev Dir" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Cancel" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Toggle" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Install" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Force Install" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prev Tab" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Next Tab" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Launching {0}" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "File Explorer" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -302,6 +330,39 @@ msgstr "" | ||||
| msgid "No cover selected" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prefix Manager" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Set" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Libraries" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Information" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Fonts" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Settings" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Winetricks not found. Please try again." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Warning" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "No components selected." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Installation failed. Check logs." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Components installed successfully." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Loading Epic Games Store games..." | ||||
| msgstr "" | ||||
|  | ||||
| @@ -350,9 +411,6 @@ msgstr "" | ||||
| msgid "Auto Install" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Emulators" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Wine Settings" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -368,6 +426,28 @@ msgstr "" | ||||
| msgid "Fullscreen" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Search" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Installation already in progress." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Failed to start installation." | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Processed {} installation..." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Installation completed successfully." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Installation failed." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Installation error." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Loading Steam games..." | ||||
| msgstr "" | ||||
|  | ||||
| @@ -380,13 +460,106 @@ msgstr "" | ||||
| msgid "Find Games ..." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Here you can configure automatic game installation..." | ||||
| #, python-brace-format | ||||
| msgid "Added '{name}'" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "List of available emulators and their configuration..." | ||||
| msgid "Compatibility tool:" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Various Wine parameters and versions..." | ||||
| msgid "Prefix:" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Wine Configuration" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Registry Editor" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Command Prompt" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Uninstaller" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Create Prefix Backup" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Load Prefix Backup" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Delete Compatibility Tool" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Delete Prefix" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Clear Prefix" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Launching tool..." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Failed to start process." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Confirm Clear" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Are you sure you want to clear prefix '{}'?" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Prefix '{}' cleared successfully." | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "" | ||||
| "Prefix '{}' cleared with errors:\n" | ||||
| "{}" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Failed to start backup process." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Failed to start restore process." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prefix backup completed." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prefix backup failed." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prefix restore completed." | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prefix restore failed." | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Are you sure you want to delete prefix '{}'?" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Prefix '{}' deleted." | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Failed to delete prefix: {}" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Are you sure you want to delete compatibility tool '{}'?" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Compatibility tool '{}' deleted." | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Failed to delete compatibility tool: {}" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Main PortProton parameters..." | ||||
| @@ -422,6 +595,9 @@ msgstr "" | ||||
| msgid "Games Display Filter:" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Gamepad Type:" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Proxy URL" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -446,6 +622,12 @@ msgstr "" | ||||
| msgid "Application Fullscreen Mode:" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Minimize to tray on close" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Application Close Mode:" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Auto Fullscreen on Gamepad connected" | ||||
| msgstr "" | ||||
|  | ||||
|   | ||||
| @@ -9,18 +9,17 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PROJECT VERSION\n" | ||||
| "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" | ||||
| "POT-Creation-Date: 2025-09-23 22:23+0500\n" | ||||
| "PO-Revision-Date: 2025-09-23 22:23+0500\n" | ||||
| "POT-Creation-Date: 2025-10-16 14:54+0500\n" | ||||
| "PO-Revision-Date: 2025-10-16 14:54+0500\n" | ||||
| "Last-Translator: \n" | ||||
| "Language-Team: ru_RU <LL@li.org>\n" | ||||
| "Language: ru_RU\n" | ||||
| "Language-Team: ru_RU <LL@li.org>\n" | ||||
| "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " | ||||
| "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" | ||||
| "MIME-Version: 1.0\n" | ||||
| "Content-Type: text/plain; charset=utf-8\n" | ||||
| "Content-Transfer-Encoding: 8bit\n" | ||||
| "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 " | ||||
| "&& (n%100<10 || n%100>=20) ? 1 : 2);\n" | ||||
| "Generated-By: Babel 2.17.0\n" | ||||
| "X-Generator: Poedit 3.6\n" | ||||
|  | ||||
| msgid "Error" | ||||
| msgstr "Ошибка" | ||||
| @@ -87,11 +86,11 @@ msgstr "Успешно" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "" | ||||
| "'{game_name}' was added to Steam. Please restart Steam for changes to take " | ||||
| "effect." | ||||
| "'{game_name}' was added to Steam. Please restart Steam for changes to " | ||||
| "take effect." | ||||
| msgstr "" | ||||
| "'{game_name}' был(а) добавлен(а) в Steam. Пожалуйста, перезапустите Steam, " | ||||
| "чтобы изменения вступили в силу." | ||||
| "'{game_name}' был(а) добавлен(а) в Steam. Пожалуйста, перезапустите " | ||||
| "Steam, чтобы изменения вступили в силу." | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Executable not found for game: {game_name}" | ||||
| @@ -179,11 +178,11 @@ msgstr "Подтвердите удаление" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "" | ||||
| "Are you sure you want to delete '{game_name}'? This will remove the .desktop " | ||||
| "file and custom data." | ||||
| "Are you sure you want to delete '{game_name}'? This will remove the " | ||||
| ".desktop file and custom data." | ||||
| msgstr "" | ||||
| "Вы уверены, что хотите удалить '{game_name}'? Это приведёт к удалению файла ." | ||||
| "desktop и пользовательских данных." | ||||
| "Вы уверены, что хотите удалить '{game_name}'? Это приведёт к удалению " | ||||
| "файла .desktop и пользовательских данных." | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Failed to delete .desktop file: {error}" | ||||
| @@ -197,6 +196,10 @@ msgstr "'{game_name}' был(а) успешно удалён(а)" | ||||
| msgid "Failed to delete custom data: {error}" | ||||
| msgstr "Не удалось удалить пользовательские данные: {error}" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Added '{game_name}' successfully" | ||||
| msgstr "'{game_name}' успешно добавлен(а)" | ||||
|  | ||||
| msgid "Game name and executable path are required" | ||||
| msgstr "Требуются название игры и путь к исполняемому файлу" | ||||
|  | ||||
| @@ -225,11 +228,11 @@ msgstr "Не удалось добавить '{game_name}' в Steam: {error}" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "" | ||||
| "'{game_name}' was removed from Steam. Please restart Steam for changes to take " | ||||
| "effect." | ||||
| "'{game_name}' was removed from Steam. Please restart Steam for changes to" | ||||
| " take effect." | ||||
| msgstr "" | ||||
| "'{game_name}' был(а) удалён(а) из Steam. Пожалуйста, перезапустите Steam, чтобы " | ||||
| "изменения вступили в силу." | ||||
| "'{game_name}' был(а) удалён(а) из Steam. Пожалуйста, перезапустите Steam," | ||||
| " чтобы изменения вступили в силу." | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Failed to remove game '{game_name}' from Steam: {error}" | ||||
| @@ -256,13 +259,37 @@ msgstr "Удалить" | ||||
| msgid "Select All" | ||||
| msgstr "Выбрать всё" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Launching {0}" | ||||
| msgstr "Идёт запуск {0}" | ||||
| msgid "Open" | ||||
| msgstr "Открыть" | ||||
|  | ||||
| msgid "Select Dir" | ||||
| msgstr "Выбрать папку" | ||||
|  | ||||
| msgid "Prev Dir" | ||||
| msgstr "Предыдущий каталог" | ||||
|  | ||||
| msgid "Cancel" | ||||
| msgstr "Отмена" | ||||
|  | ||||
| msgid "Toggle" | ||||
| msgstr "Переключить" | ||||
|  | ||||
| msgid "Install" | ||||
| msgstr "Установить" | ||||
|  | ||||
| msgid "Force Install" | ||||
| msgstr "Принудительно установить" | ||||
|  | ||||
| msgid "Prev Tab" | ||||
| msgstr "Предыдущая вкладка" | ||||
|  | ||||
| msgid "Next Tab" | ||||
| msgstr "Следующая вкладка" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Launching {0}" | ||||
| msgstr "Идёт запуск {0}" | ||||
|  | ||||
| msgid "File Explorer" | ||||
| msgstr "Проводник" | ||||
|  | ||||
| @@ -274,7 +301,7 @@ msgstr "Путь: " | ||||
|  | ||||
| #, python-format | ||||
| msgid "Access denied: %s" | ||||
| msgstr "Доступ запрещен: %s" | ||||
| msgstr "Доступ запрещён: %s" | ||||
|  | ||||
| msgid "Edit Game" | ||||
| msgstr "Редактировать игру" | ||||
| @@ -312,6 +339,39 @@ msgstr "Скачивание обложки..." | ||||
| msgid "No cover selected" | ||||
| msgstr "Обложка не выбрана" | ||||
|  | ||||
| msgid "Prefix Manager" | ||||
| msgstr "Менеджер префиксов" | ||||
|  | ||||
| msgid "Set" | ||||
| msgstr "Выбор" | ||||
|  | ||||
| msgid "Libraries" | ||||
| msgstr "Библиотеки" | ||||
|  | ||||
| msgid "Information" | ||||
| msgstr "Описание" | ||||
|  | ||||
| msgid "Fonts" | ||||
| msgstr "Шрифты" | ||||
|  | ||||
| msgid "Settings" | ||||
| msgstr "Настройки" | ||||
|  | ||||
| msgid "Winetricks not found. Please try again." | ||||
| msgstr "Winetricks не найден. Повторите попытку." | ||||
|  | ||||
| msgid "Warning" | ||||
| msgstr "Предупреждение" | ||||
|  | ||||
| msgid "No components selected." | ||||
| msgstr "Не выбрано ни одного компонента." | ||||
|  | ||||
| msgid "Installation failed. Check logs." | ||||
| msgstr "Установка не удалась. Проверьте журналы." | ||||
|  | ||||
| msgid "Components installed successfully." | ||||
| msgstr "Компоненты успешно установлены." | ||||
|  | ||||
| msgid "Loading Epic Games Store games..." | ||||
| msgstr "Загрузка игр из Epic Games Store..." | ||||
|  | ||||
| @@ -360,9 +420,6 @@ msgstr "Библиотека" | ||||
| msgid "Auto Install" | ||||
| msgstr "Автоустановка" | ||||
|  | ||||
| msgid "Emulators" | ||||
| msgstr "Эмуляторы" | ||||
|  | ||||
| msgid "Wine Settings" | ||||
| msgstr "Настройки wine" | ||||
|  | ||||
| @@ -378,6 +435,28 @@ msgstr "Назад" | ||||
| msgid "Fullscreen" | ||||
| msgstr "Полный экран" | ||||
|  | ||||
| msgid "Search" | ||||
| msgstr "Поиск" | ||||
|  | ||||
| msgid "Installation already in progress." | ||||
| msgstr "Установка уже выполняется." | ||||
|  | ||||
| msgid "Failed to start installation." | ||||
| msgstr "Не удалось запустить установку." | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Processed {} installation..." | ||||
| msgstr "В процессе установки {}..." | ||||
|  | ||||
| msgid "Installation completed successfully." | ||||
| msgstr "Установка завершена успешно." | ||||
|  | ||||
| msgid "Installation failed." | ||||
| msgstr "Установка не удалась." | ||||
|  | ||||
| msgid "Installation error." | ||||
| msgstr "Ошибка установки." | ||||
|  | ||||
| msgid "Loading Steam games..." | ||||
| msgstr "Загрузка игр из Steam..." | ||||
|  | ||||
| @@ -390,14 +469,109 @@ msgstr "Игровая библиотека" | ||||
| msgid "Find Games ..." | ||||
| msgstr "Найти игры..." | ||||
|  | ||||
| msgid "Here you can configure automatic game installation..." | ||||
| msgstr "Здесь можно настроить автоматическую установку игр..." | ||||
| #, python-brace-format | ||||
| msgid "Added '{name}'" | ||||
| msgstr "'{name}' добавлен(а)" | ||||
|  | ||||
| msgid "List of available emulators and their configuration..." | ||||
| msgstr "Список доступных эмуляторов и их настройка..." | ||||
| msgid "Compatibility tool:" | ||||
| msgstr "Инструмент совместимости:" | ||||
|  | ||||
| msgid "Various Wine parameters and versions..." | ||||
| msgstr "Различные параметры и версии wine..." | ||||
| msgid "Prefix:" | ||||
| msgstr "Префикс:" | ||||
|  | ||||
| msgid "Wine Configuration" | ||||
| msgstr "Конфигурация Wine" | ||||
|  | ||||
| msgid "Registry Editor" | ||||
| msgstr "Редактор реестра" | ||||
|  | ||||
| msgid "Command Prompt" | ||||
| msgstr "Командная строка" | ||||
|  | ||||
| msgid "Uninstaller" | ||||
| msgstr "Удаление программ" | ||||
|  | ||||
| msgid "Create Prefix Backup" | ||||
| msgstr "Создать резервную копию префикса" | ||||
|  | ||||
| msgid "Load Prefix Backup" | ||||
| msgstr "Загрузить резервную копию префикса" | ||||
|  | ||||
| msgid "Delete Compatibility Tool" | ||||
| msgstr "Удалить Инструмент совместимости" | ||||
|  | ||||
| msgid "Delete Prefix" | ||||
| msgstr "Удалить Префикс" | ||||
|  | ||||
| msgid "Clear Prefix" | ||||
| msgstr "Очистить Префикс" | ||||
|  | ||||
| msgid "Launching tool..." | ||||
| msgstr "Запуск инструмента..." | ||||
|  | ||||
| msgid "Failed to start process." | ||||
| msgstr "Не удалось запустить процесс." | ||||
|  | ||||
| msgid "Confirm Clear" | ||||
| msgstr "Подтвердите очистку" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Are you sure you want to clear prefix '{}'?" | ||||
| msgstr "Вы уверены, что хотите очистить префикс «{}»?" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Prefix '{}' cleared successfully." | ||||
| msgstr "Префикс '{}' успешно удален." | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "" | ||||
| "Prefix '{}' cleared with errors:\n" | ||||
| "{}" | ||||
| msgstr "" | ||||
| "Префикс '{}' очищен с ошибками:\n" | ||||
| "{}" | ||||
|  | ||||
| msgid "Failed to start backup process." | ||||
| msgstr "Не удалось запустить процесс резервного копирования." | ||||
|  | ||||
| msgid "Failed to start restore process." | ||||
| msgstr "Не удалось запустить процесс восстановления." | ||||
|  | ||||
| msgid "Prefix backup completed." | ||||
| msgstr "Резервное копирование префикса завершено." | ||||
|  | ||||
| msgid "Prefix backup failed." | ||||
| msgstr "Сбой резервного копирования префикса." | ||||
|  | ||||
| msgid "Prefix restore completed." | ||||
| msgstr "Восстановление префикса завершено." | ||||
|  | ||||
| msgid "Prefix restore failed." | ||||
| msgstr "Восстановление префикса не удалось." | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Are you sure you want to delete prefix '{}'?" | ||||
| msgstr "Вы уверены, что хотите удалить префикс «{}»?" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Prefix '{}' deleted." | ||||
| msgstr "Префикс «{}» удален." | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Failed to delete prefix: {}" | ||||
| msgstr "Не удалось удалить префикс: {}" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Are you sure you want to delete compatibility tool '{}'?" | ||||
| msgstr "Вы уверены, что хотите удалить инструмент совместимости «{}»?" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Compatibility tool '{}' deleted." | ||||
| msgstr "Инструмент совместимости «{}» удален." | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Failed to delete compatibility tool: {}" | ||||
| msgstr "Не удалось удалить инструмент совместимости: {}" | ||||
|  | ||||
| msgid "Main PortProton parameters..." | ||||
| msgstr "Основные параметры PortProton..." | ||||
| @@ -432,6 +606,9 @@ msgstr "все" | ||||
| msgid "Games Display Filter:" | ||||
| msgstr "Фильтр игр:" | ||||
|  | ||||
| msgid "Gamepad Type:" | ||||
| msgstr "Тип геймпада:" | ||||
|  | ||||
| msgid "Proxy URL" | ||||
| msgstr "Адрес прокси" | ||||
|  | ||||
| @@ -456,6 +633,12 @@ msgstr "Запуск приложения в полноэкранном режи | ||||
| msgid "Application Fullscreen Mode:" | ||||
| msgstr "Режим полноэкранного отображения приложения:" | ||||
|  | ||||
| msgid "Minimize to tray on close" | ||||
| msgstr "Сворачивать в трей при закрытии" | ||||
|  | ||||
| msgid "Application Close Mode:" | ||||
| msgstr "Режим закрытия приложения:" | ||||
|  | ||||
| msgid "Auto Fullscreen on Gamepad connected" | ||||
| msgstr "Режим полноэкранного отображения приложения при подключении геймпада" | ||||
|  | ||||
| @@ -482,7 +665,8 @@ msgstr "Подтвердите удаление" | ||||
|  | ||||
| msgid "Are you sure you want to reset all settings? This action cannot be undone." | ||||
| msgstr "" | ||||
| "Вы уверены, что хотите сбросить все настройки? Это действие нельзя отменить." | ||||
| "Вы уверены, что хотите сбросить все настройки? Это действие нельзя " | ||||
| "отменить." | ||||
|  | ||||
| msgid "Settings reset. Restarting..." | ||||
| msgstr "Настройки сброшены. Перезапуск..." | ||||
| @@ -654,3 +838,4 @@ msgstr "Нет избранных" | ||||
|  | ||||
| msgid "No recent games" | ||||
| msgstr "Нет недавних игр" | ||||
|  | ||||
|   | ||||
| @@ -4,12 +4,18 @@ import orjson | ||||
| import requests | ||||
| import urllib.parse | ||||
| import time | ||||
| import glob | ||||
| import re | ||||
| import hashlib | ||||
| from collections.abc import Callable | ||||
| from PySide6.QtCore import QThread, Signal | ||||
| from portprotonqt.downloader import Downloader | ||||
| from portprotonqt.logger import get_logger | ||||
| from portprotonqt.config_utils import get_portproton_location | ||||
|  | ||||
| logger = get_logger(__name__) | ||||
| CACHE_DURATION = 30 * 24 * 60 * 60  # 30 days in seconds | ||||
| AUTOINSTALL_CACHE_DURATION = 3600  # 1 hour for autoinstall cache | ||||
|  | ||||
| def normalize_name(s): | ||||
|     """ | ||||
| @@ -52,7 +58,11 @@ class PortProtonAPI: | ||||
|         self.xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share")) | ||||
|         self.custom_data_dir = os.path.join(self.xdg_data_home, "PortProtonQt", "custom_data") | ||||
|         os.makedirs(self.custom_data_dir, exist_ok=True) | ||||
|         self.portproton_location = get_portproton_location() | ||||
|         self.repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | ||||
|         self.builtin_custom_folder = os.path.join(self.repo_root, "custom_data") | ||||
|         self._topics_data = None | ||||
|         self._autoinstall_cache = None  # New: In-memory cache | ||||
|  | ||||
|     def _get_game_dir(self, exe_name: str) -> str: | ||||
|         game_dir = os.path.join(self.custom_data_dir, exe_name) | ||||
| @@ -68,40 +78,6 @@ class PortProtonAPI: | ||||
|             logger.debug(f"Failed to check file at {url}: {e}") | ||||
|             return False | ||||
|  | ||||
|     def download_game_assets(self, exe_name: str, timeout: int = 5) -> dict[str, str | None]: | ||||
|         game_dir = self._get_game_dir(exe_name) | ||||
|         results: dict[str, str | None] = {"cover": None, "metadata": None} | ||||
|         cover_extensions = [".png", ".jpg", ".jpeg", ".bmp"] | ||||
|         cover_url_base = f"{self.base_url}/{exe_name}/cover" | ||||
|         metadata_url = f"{self.base_url}/{exe_name}/metadata.txt" | ||||
|  | ||||
|         for ext in cover_extensions: | ||||
|             cover_url = f"{cover_url_base}{ext}" | ||||
|             if self._check_file_exists(cover_url, timeout): | ||||
|                 local_cover_path = os.path.join(game_dir, f"cover{ext}") | ||||
|                 result = self.downloader.download(cover_url, local_cover_path, timeout=timeout) | ||||
|                 if result: | ||||
|                     results["cover"] = result | ||||
|                     logger.info(f"Downloaded cover for {exe_name} to {result}") | ||||
|                     break | ||||
|                 else: | ||||
|                     logger.error(f"Failed to download cover for {exe_name} from {cover_url}") | ||||
|             else: | ||||
|                 logger.debug(f"No cover found for {exe_name} with extension {ext}") | ||||
|  | ||||
|         if self._check_file_exists(metadata_url, timeout): | ||||
|             local_metadata_path = os.path.join(game_dir, "metadata.txt") | ||||
|             result = self.downloader.download(metadata_url, local_metadata_path, timeout=timeout) | ||||
|             if result: | ||||
|                 results["metadata"] = result | ||||
|                 logger.info(f"Downloaded metadata for {exe_name} to {result}") | ||||
|             else: | ||||
|                 logger.error(f"Failed to download metadata for {exe_name} from {metadata_url}") | ||||
|         else: | ||||
|             logger.debug(f"No metadata found for {exe_name}") | ||||
|  | ||||
|         return results | ||||
|  | ||||
|     def download_game_assets_async(self, exe_name: str, timeout: int = 5, callback: Callable[[dict[str, str | None]], None] | None = None) -> None: | ||||
|         game_dir = self._get_game_dir(exe_name) | ||||
|         cover_extensions = [".png", ".jpg", ".jpeg", ".bmp"] | ||||
| @@ -163,6 +139,236 @@ class PortProtonAPI: | ||||
|             if callback: | ||||
|                 callback(results) | ||||
|  | ||||
|     def download_autoinstall_cover_async(self, exe_name: str, timeout: int = 5, callback: Callable[[str | None], None] | None = None) -> None: | ||||
|         """Download only autoinstall cover image (PNG only, no metadata).""" | ||||
|         xdg_data_home = os.getenv("XDG_DATA_HOME", | ||||
|                                 os.path.join(os.path.expanduser("~"), ".local", "share")) | ||||
|         autoinstall_root = os.path.join(xdg_data_home, "PortProtonQt", "custom_data", "autoinstall") | ||||
|         user_game_folder = os.path.join(autoinstall_root, exe_name) | ||||
|  | ||||
|         if not os.path.isdir(user_game_folder): | ||||
|             try: | ||||
|                 os.mkdir(user_game_folder) | ||||
|             except FileExistsError: | ||||
|                 pass | ||||
|  | ||||
|         cover_url = f"{self.base_url}/{exe_name}/cover.png" | ||||
|         local_cover_path = os.path.join(user_game_folder, "cover.png") | ||||
|  | ||||
|         def on_cover_downloaded(local_path: str | None): | ||||
|             if local_path: | ||||
|                 logger.info(f"Async autoinstall cover downloaded for {exe_name}: {local_path}") | ||||
|             else: | ||||
|                 logger.debug(f"No autoinstall cover downloaded for {exe_name}") | ||||
|             if callback: | ||||
|                 callback(local_path) | ||||
|  | ||||
|         if self._check_file_exists(cover_url, timeout): | ||||
|             self.downloader.download_async( | ||||
|                 cover_url, | ||||
|                 local_cover_path, | ||||
|                 timeout=timeout, | ||||
|                 callback=on_cover_downloaded | ||||
|             ) | ||||
|         else: | ||||
|             logger.debug(f"No autoinstall cover found for {exe_name}") | ||||
|             if callback: | ||||
|                 callback(None) | ||||
|  | ||||
|     def parse_autoinstall_script(self, file_path: str) -> tuple[str | None, str | None]: | ||||
|         """Extract display_name from # name comment and exe_name from autoinstall bash script.""" | ||||
|         try: | ||||
|             with open(file_path, encoding='utf-8') as f: | ||||
|                 content = f.read() | ||||
|  | ||||
|             # Skip emulators | ||||
|             if re.search(r'#\s*type\s*:\s*emulators', content, re.IGNORECASE): | ||||
|                 return None, None | ||||
|  | ||||
|             display_name = None | ||||
|             exe_name = None | ||||
|  | ||||
|             # Extract display_name from "# name:" comment | ||||
|             name_match = re.search(r'#\s*name\s*:\s*(.+)', content, re.IGNORECASE) | ||||
|             if name_match: | ||||
|                 display_name = name_match.group(1).strip() | ||||
|  | ||||
|             # --- pw_create_unique_exe --- | ||||
|             pw_match = re.search(r'pw_create_unique_exe(?:\s+["\']([^"\']+)["\'])?', content) | ||||
|             if pw_match: | ||||
|                 arg = pw_match.group(1) | ||||
|                 if arg: | ||||
|                     exe_name = arg.strip() | ||||
|                     if not exe_name.lower().endswith(".exe"): | ||||
|                         exe_name += ".exe" | ||||
|                 else: | ||||
|                     export_match = re.search( | ||||
|                         r'export\s+PORTWINE_CREATE_SHORTCUT_NAME\s*=\s*["\']([^"\']+)["\']', | ||||
|                         content, re.IGNORECASE) | ||||
|                     if export_match: | ||||
|                         exe_name = f"{export_match.group(1).strip()}.exe" | ||||
|  | ||||
|             else: | ||||
|                 portwine_match = None | ||||
|                 for line in content.splitlines(): | ||||
|                     stripped = line.strip() | ||||
|                     if stripped.startswith("#"): | ||||
|                         continue | ||||
|                     if "portwine_exe" in stripped and "=" in stripped: | ||||
|                         portwine_match = stripped | ||||
|                         break | ||||
|  | ||||
|                 if portwine_match: | ||||
|                     exe_expr = portwine_match.split("=", 1)[1].strip().strip("'\" ") | ||||
|                     exe_candidates = re.findall(r'[-\w\s/\\\.]+\.exe', exe_expr) | ||||
|                     if exe_candidates: | ||||
|                         exe_name = os.path.basename(exe_candidates[-1].strip()) | ||||
|  | ||||
|  | ||||
|             # Fallback | ||||
|             if not display_name and exe_name: | ||||
|                 display_name = exe_name | ||||
|  | ||||
|             return display_name, exe_name | ||||
|  | ||||
|         except Exception as e: | ||||
|             logger.error(f"Failed to parse {file_path}: {e}") | ||||
|             return None, None | ||||
|  | ||||
|     def _compute_scripts_signature(self, auto_dir: str) -> str: | ||||
|         """Compute a hash-based signature of the autoinstall scripts to detect changes.""" | ||||
|         if not os.path.exists(auto_dir): | ||||
|             return "" | ||||
|         scripts = sorted(glob.glob(os.path.join(auto_dir, "*"))) | ||||
|         # Simple hash: concatenate sorted filenames and hash | ||||
|         filenames_str = "".join(sorted([os.path.basename(s) for s in scripts])) | ||||
|         return hashlib.md5(filenames_str.encode()).hexdigest() | ||||
|  | ||||
|     def _load_autoinstall_cache(self): | ||||
|         """Load cached autoinstall games if fresh and scripts unchanged.""" | ||||
|         if self._autoinstall_cache is not None: | ||||
|             return self._autoinstall_cache | ||||
|         cache_dir = get_cache_dir() | ||||
|         cache_file = os.path.join(cache_dir, "autoinstall_games_cache.json") | ||||
|         if os.path.exists(cache_file): | ||||
|             try: | ||||
|                 mod_time = os.path.getmtime(cache_file) | ||||
|                 if time.time() - mod_time < AUTOINSTALL_CACHE_DURATION: | ||||
|                     with open(cache_file, "rb") as f: | ||||
|                         data = orjson.loads(f.read()) | ||||
|                         # Check signature | ||||
|                         cached_signature = data.get("scripts_signature", "") | ||||
|                         current_signature = self._compute_scripts_signature( | ||||
|                             os.path.join(self.portproton_location or "", "data", "scripts", "pw_autoinstall") | ||||
|                         ) | ||||
|                         if cached_signature != current_signature: | ||||
|                             logger.info("Scripts signature mismatch; invalidating cache") | ||||
|                             return None | ||||
|                         self._autoinstall_cache = data["games"] | ||||
|                         logger.info(f"Loaded {len(self._autoinstall_cache)} cached autoinstall games") | ||||
|                         return self._autoinstall_cache | ||||
|             except Exception as e: | ||||
|                 logger.error(f"Failed to load autoinstall cache: {e}") | ||||
|         return None | ||||
|  | ||||
|     def _save_autoinstall_cache(self, games): | ||||
|         """Save parsed autoinstall games to cache with scripts signature.""" | ||||
|         try: | ||||
|             cache_dir = get_cache_dir() | ||||
|             cache_file = os.path.join(cache_dir, "autoinstall_games_cache.json") | ||||
|             auto_dir = os.path.join(self.portproton_location or "", "data", "scripts", "pw_autoinstall") | ||||
|             scripts_signature = self._compute_scripts_signature(auto_dir) | ||||
|             data = {"games": games, "scripts_signature": scripts_signature, "timestamp": time.time()} | ||||
|             with open(cache_file, "wb") as f: | ||||
|                 f.write(orjson.dumps(data)) | ||||
|             logger.debug(f"Saved {len(games)} autoinstall games to cache with signature {scripts_signature}") | ||||
|         except Exception as e: | ||||
|             logger.error(f"Failed to save autoinstall cache: {e}") | ||||
|  | ||||
|     def start_autoinstall_games_load(self, callback: Callable[[list[tuple]], None]) -> QThread | None: | ||||
|         """Start loading auto-install games in a background thread. Returns the thread for management.""" | ||||
|         # Check cache first (sync, fast) | ||||
|         cached_games = self._load_autoinstall_cache() | ||||
|         if cached_games is not None: | ||||
|             # Emit via callback immediately if cached | ||||
|             QThread.msleep(0)  # Yield to Qt event loop | ||||
|             callback(cached_games) | ||||
|             return None  # No thread needed | ||||
|  | ||||
|         # No cache: Start background thread | ||||
|         class AutoinstallWorker(QThread): | ||||
|             finished = Signal(list) | ||||
|             api: "PortProtonAPI" | ||||
|             portproton_location: str | None | ||||
|  | ||||
|             def run(self): | ||||
|                 games = [] | ||||
|                 auto_dir = os.path.join( | ||||
|                     self.portproton_location or "", "data", "scripts", "pw_autoinstall" | ||||
|                 ) if self.portproton_location else "" | ||||
|                 if not os.path.exists(auto_dir): | ||||
|                     self.finished.emit(games) | ||||
|                     return | ||||
|  | ||||
|                 scripts = sorted(glob.glob(os.path.join(auto_dir, "*"))) | ||||
|                 if not scripts: | ||||
|                     self.finished.emit(games) | ||||
|                     return | ||||
|  | ||||
|                 xdg_data_home = os.getenv( | ||||
|                     "XDG_DATA_HOME", | ||||
|                     os.path.join(os.path.expanduser("~"), ".local", "share"), | ||||
|                 ) | ||||
|                 base_autoinstall_dir = os.path.join( | ||||
|                     xdg_data_home, "PortProtonQt", "custom_data", "autoinstall" | ||||
|                 ) | ||||
|                 os.makedirs(base_autoinstall_dir, exist_ok=True) | ||||
|  | ||||
|                 for script_path in scripts: | ||||
|                     display_name, exe_name = self.api.parse_autoinstall_script(script_path) | ||||
|                     script_name = os.path.splitext(os.path.basename(script_path))[0] | ||||
|  | ||||
|                     if not (display_name and exe_name): | ||||
|                         continue | ||||
|  | ||||
|                     exe_name = os.path.splitext(exe_name)[0] | ||||
|                     user_game_folder = os.path.join(base_autoinstall_dir, exe_name) | ||||
|                     os.makedirs(user_game_folder, exist_ok=True) | ||||
|  | ||||
|                     # Find cover | ||||
|                     cover_path = "" | ||||
|                     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}" | ||||
|                         if candidate in user_files: | ||||
|                             cover_path = os.path.join(user_game_folder, candidate) | ||||
|                             break | ||||
|  | ||||
|                     if not cover_path: | ||||
|                         logger.debug(f"No local cover found for autoinstall {exe_name}") | ||||
|  | ||||
|                     game_tuple = ( | ||||
|                         display_name, "", cover_path, "", f"autoinstall:{script_name}", | ||||
|                         "", "Never", "0h 0m", "", "", 0, 0, "autoinstall", exe_name | ||||
|                     ) | ||||
|                     games.append(game_tuple) | ||||
|  | ||||
|                 self.api._save_autoinstall_cache(games) | ||||
|                 self.api._autoinstall_cache = games | ||||
|                 self.finished.emit(games) | ||||
|  | ||||
|         worker = AutoinstallWorker() | ||||
|         worker.api = self | ||||
|         worker.portproton_location = self.portproton_location | ||||
|         worker.finished.connect(lambda games: callback(games)) | ||||
|         worker.start() | ||||
|         logger.info("Started background load of autoinstall games") | ||||
|         return worker | ||||
|  | ||||
|     def _load_topics_data(self): | ||||
|         """Load and cache linux_gaming_topics_min.json from the archive.""" | ||||
|         if self._topics_data is not None: | ||||
|   | ||||
							
								
								
									
										49
									
								
								portprotonqt/preloader.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,49 @@ | ||||
| import time | ||||
|  | ||||
| from PySide6.QtCore import QRect | ||||
| from PySide6.QtGui import QPainter, QPen, QBrush, Qt, QColor, QConicalGradient | ||||
| from PySide6.QtWidgets import QWidget | ||||
|  | ||||
| class Preloader(QWidget): | ||||
|     def __init__(self, speed=180.0, line_line_width=20, color=QColor(0, 120, 215), parent=None): | ||||
|         super().__init__(parent) | ||||
|         self.setFixedSize(150, 150) | ||||
|         self._speed = speed | ||||
|         self._line_width = line_line_width | ||||
|         self._color1 = color | ||||
|         self._color2 = QColor(color.red(), color.green(), color.blue(), 0) | ||||
|         self._start_time = time.time() | ||||
|  | ||||
|     def showEvent(self, event): | ||||
|         self._start_time = time.time() | ||||
|  | ||||
|     def paintEvent(self, event): | ||||
|         rect = self._get_preloader_rect() | ||||
|         center = rect.center() | ||||
|         painter = QPainter(self) | ||||
|         painter.setRenderHint(QPainter.RenderHint.Antialiasing) | ||||
|         painter.setPen(self._get_pen()) | ||||
|         painter.translate(center) | ||||
|         painter.rotate(self._get_angle()) | ||||
|         painter.translate(-center) | ||||
|         painter.drawArc(rect, 0, 270 * 16) | ||||
|         self.update() | ||||
|  | ||||
|     def _get_pen(self) -> QPen: | ||||
|         gradient = QConicalGradient() | ||||
|         gradient.setCenter(self.rect().center()) | ||||
|         gradient.setColorAt(0, self._color1) | ||||
|         gradient.setColorAt(1, self._color2) | ||||
|         pen = QPen(QBrush(gradient), self._line_width) | ||||
|         pen.setCapStyle(Qt.PenCapStyle.RoundCap) | ||||
|         return pen | ||||
|  | ||||
|     def _get_angle(self) -> float: | ||||
|         duration = time.time() - self._start_time | ||||
|         return (self._speed * duration) % 360.0 | ||||
|  | ||||
|     def _get_preloader_rect(self) -> QRect: | ||||
|         size = self._line_width // 2 | ||||
|         rect = self.rect() | ||||
|         rect.adjust(size, size, -size, -size) | ||||
|         return rect | ||||
| @@ -13,7 +13,7 @@ from portprotonqt.logger import get_logger | ||||
| from portprotonqt.localization import get_steam_language | ||||
| from portprotonqt.downloader import Downloader | ||||
| from portprotonqt.dialogs import generate_thumbnail | ||||
| from portprotonqt.config_utils import get_portproton_location | ||||
| from portprotonqt.config_utils import get_portproton_location, get_portproton_start_command | ||||
| from collections.abc import Callable | ||||
| import re | ||||
| import shutil | ||||
| @@ -23,6 +23,7 @@ import requests | ||||
| import random | ||||
| import base64 | ||||
| import glob | ||||
| import urllib.parse | ||||
|  | ||||
| downloader = Downloader() | ||||
| logger = get_logger(__name__) | ||||
| @@ -211,14 +212,28 @@ def normalize_name(s): | ||||
|  | ||||
| def is_valid_candidate(candidate): | ||||
|     """ | ||||
|     Checks if a candidate contains forbidden substrings: | ||||
|       - win32 | ||||
|       - win64 | ||||
|       - gamelauncher | ||||
|     Additionally checks the string without spaces. | ||||
|     Returns True if the candidate is valid, otherwise False. | ||||
|     Determines whether a given candidate string is valid for use as a game name. | ||||
|  | ||||
|     The function performs the following checks: | ||||
|       1. Normalizes the candidate using `normalize_name()`. | ||||
|       2. Rejects the candidate if the normalized name is exactly "game" | ||||
|          (to avoid overly generic names). | ||||
|       3. Removes spaces and checks for forbidden substrings: | ||||
|          - "win32" | ||||
|          - "win64" | ||||
|          - "gamelauncher" | ||||
|          These are checked in the space-free version of the string. | ||||
|       4. Returns True only if none of the forbidden conditions are met. | ||||
|  | ||||
|     Args: | ||||
|         candidate (str): The candidate string to validate. | ||||
|  | ||||
|     Returns: | ||||
|         bool: True if the candidate is valid, False otherwise. | ||||
|     """ | ||||
|     normalized_candidate = normalize_name(candidate) | ||||
|     if normalized_candidate == "game": | ||||
|         return False | ||||
|     normalized_no_space = normalized_candidate.replace(" ", "") | ||||
|     forbidden = ["win32", "win64", "gamelauncher"] | ||||
|     for token in forbidden: | ||||
| @@ -397,6 +412,39 @@ def save_app_details(app_id, data): | ||||
|     with open(cache_file, "wb") as f: | ||||
|         f.write(orjson.dumps(data)) | ||||
|  | ||||
| def fetch_sgdb_cover(game_name: str) -> str: | ||||
|     """ | ||||
|     Fetch a cover image URL from steamgrid.usebottles.com for the given game. | ||||
|     The API returns a single string (quoted URL). | ||||
|     """ | ||||
|     try: | ||||
|         encoded = urllib.parse.quote(game_name) | ||||
|         url = f"https://steamgrid.usebottles.com/api/search/{encoded}" | ||||
|         resp = requests.get(url, timeout=5) | ||||
|         if resp.status_code != 200: | ||||
|             logger.warning("SGDB request failed for %s: %s", game_name, resp.status_code) | ||||
|             return "" | ||||
|         text = resp.text.strip() | ||||
|         # Убираем возможные кавычки вокруг строки | ||||
|         if text.startswith('"') and text.endswith('"'): | ||||
|             text = text[1:-1] | ||||
|         if text: | ||||
|             logger.info("Fetched SGDB cover for %s: %s", game_name, text) | ||||
|         return text | ||||
|     except Exception as e: | ||||
|         logger.warning("Failed to fetch SGDB cover for %s: %s", game_name, e) | ||||
|     return "" | ||||
|  | ||||
|  | ||||
| def check_url_exists(url: str) -> bool: | ||||
|     """Check whether a URL returns HTTP 200.""" | ||||
|     try: | ||||
|         r = requests.head(url, timeout=3) | ||||
|         return r.status_code == 200 | ||||
|     except Exception: | ||||
|         return False | ||||
|  | ||||
|  | ||||
| def fetch_app_info_async(app_id: int, callback: Callable[[dict | None], None]): | ||||
|     """ | ||||
|     Asynchronously fetches detailed app info from Steam API. | ||||
| @@ -615,6 +663,11 @@ def get_full_steam_game_info_async(appid: int, callback: Callable[[dict], None]) | ||||
|         title = decode_text(app_info.get("name", "")) | ||||
|         description = decode_text(app_info.get("short_description", "")) | ||||
|         cover = f"https://steamcdn-a.akamaihd.net/steam/apps/{appid}/library_600x900_2x.jpg" | ||||
|         if not check_url_exists(cover): | ||||
|             logger.info("Steam cover not found for %s, trying SGDB", title) | ||||
|             alt_cover = fetch_sgdb_cover(title) | ||||
|             if alt_cover: | ||||
|                 cover = alt_cover | ||||
|  | ||||
|         def on_protondb_tier(tier: str): | ||||
|             def on_anticheat_status(anticheat_status: str): | ||||
| @@ -708,12 +761,15 @@ def get_steam_game_info_async(desktop_name: str, exec_line: str, callback: Calla | ||||
|         game_name = desktop_name or exe_name.capitalize() | ||||
|  | ||||
|         if not matching_app: | ||||
|             cover = fetch_sgdb_cover(game_name) or "" | ||||
|             logger.info("Using SGDB cover for non-Steam game '%s': %s", game_name, cover) | ||||
|  | ||||
|             def on_anticheat_status(anticheat_status: str): | ||||
|                 callback({ | ||||
|                     "appid": "", | ||||
|                     "name": decode_text(game_name), | ||||
|                     "description": "", | ||||
|                     "cover": "", | ||||
|                     "cover": cover, | ||||
|                     "controller_support": "", | ||||
|                     "protondb_tier": "", | ||||
|                     "steam_game": "false", | ||||
| @@ -744,6 +800,11 @@ def get_steam_game_info_async(desktop_name: str, exec_line: str, callback: Calla | ||||
|             title = decode_text(app_info.get("name", game_name)) | ||||
|             description = decode_text(app_info.get("short_description", "")) | ||||
|             cover = f"https://steamcdn-a.akamaihd.net/steam/apps/{appid}/library_600x900_2x.jpg" | ||||
|             if not check_url_exists(cover): | ||||
|                 logger.info("Steam cover not found for %s, trying SGDB", title) | ||||
|                 alt_cover = fetch_sgdb_cover(title) | ||||
|                 if alt_cover: | ||||
|                     cover = alt_cover | ||||
|             controller_support = app_info.get("controller_support", "") | ||||
|  | ||||
|             def on_protondb_tier(tier: str): | ||||
| @@ -943,7 +1004,8 @@ def add_to_steam(game_name: str, exec_line: str, cover_path: str) -> tuple[bool, | ||||
|         return (False, f"Executable file not found: {exe_path}") | ||||
|  | ||||
|     portproton_dir = get_portproton_location() | ||||
|     if not portproton_dir: | ||||
|     start_sh = get_portproton_start_command() | ||||
|     if not portproton_dir or not start_sh: | ||||
|         logger.error("PortProton directory not found") | ||||
|         return (False, "PortProton directory not found") | ||||
|  | ||||
| @@ -952,17 +1014,12 @@ def add_to_steam(game_name: str, exec_line: str, cover_path: str) -> tuple[bool, | ||||
|  | ||||
|     safe_game_name = re.sub(r'[<>:"/\\|?*]', '_', game_name.strip()) | ||||
|     script_path = os.path.join(steam_scripts_dir, f"{safe_game_name}.sh") | ||||
|     start_sh_path = os.path.join(portproton_dir, "data", "scripts", "start.sh") | ||||
|  | ||||
|     if not os.path.exists(start_sh_path): | ||||
|         logger.error(f"start.sh not found at {start_sh_path}") | ||||
|         return (False, f"start.sh not found at {start_sh_path}") | ||||
|  | ||||
|     if not os.path.exists(script_path): | ||||
|         script_content = f"""#!/usr/bin/env bash | ||||
| export LD_PRELOAD= | ||||
| export START_FROM_STEAM=1 | ||||
| "{start_sh_path}" "{exe_path}" "$@" | ||||
| "{start_sh}" "{exe_path}" "$@" | ||||
| """ | ||||
|         try: | ||||
|             with open(script_path, "w", encoding="utf-8") as f: | ||||
|   | ||||
							
								
								
									
										1
									
								
								portprotonqt/themes/standart/images/icons/settings.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m8.0005 1c-0.38761 0-0.77522 0.0327-1.1588 0.0979-0.16351 0.0281-0.30273 0.13627-0.37209 0.28935l-0.39088 0.86264c-0.49378 0.16682-0.96454 0.39759-1.4007 0.68616 2.5e-4 0-0.90672-0.2272-0.90672-0.2272-0.161-0.0403-0.33098 3e-3 -0.45442 0.11569-0.57867 0.5285-1.0672 1.1514-1.4451 1.8432-0.0804 0.14721-0.0841 0.32549-0.01 0.47628l0.41938 0.84865c-0.17954 0.49666-0.29567 1.0147-0.346 1.5417l-0.73995 0.57946c-0.13121 0.10289-0.20407 0.26514-0.19431 0.4335 0.0453 0.78981 0.21961 1.5666 0.51558 2.2983 0.0631 0.15587 0.1978 0.27003 0.36005 0.30467l0.91397 0.19559c0.26993 0.45234 0.59572 0.86802 0.96931 1.2363l-0.0161 0.94973c-3e-3 0.16861 0.0766 0.32755 0.21183 0.42484 0.63551 0.45642 1.3414 0.80207 2.0884 1.0229 0.15926 0.0471 0.33077 0.0109 0.45872-0.0963l0.72016-0.60485c0.51582 0.0674 1.0384 0.0674 1.5544 0l0.72016 0.60485c0.12796 0.10722 0.29946 0.14343 0.45872 0.0963 0.74693-0.22083 1.4528-0.56648 2.0883-1.0229 0.13521-0.0973 0.21465-0.25623 0.21189-0.42484l-0.0161-0.94973c0.37359-0.36829 0.69939-0.78372 0.96932-1.2363l0.91396-0.19559c0.16226-0.0347 0.29695-0.1488 0.36005-0.30467 0.29597-0.73174 0.47026-1.5085 0.51558-2.2983 0.01-0.16836-0.0631-0.33061-0.1943-0.4335l-0.73996-0.57946c-0.0501-0.52671-0.16652-1.045-0.34606-1.5417l0.41944-0.84865c0.0746-0.15079 0.0709-0.32907-0.01-0.47628-0.37785-0.69176-0.86638-1.3147-1.445-1.8432-0.12345-0.11258-0.29343-0.15594-0.45443-0.11569l-0.90697 0.2272c-0.43594-0.28857-0.9067-0.51908-1.4005-0.68616l-0.39088-0.86264c-0.0694-0.15308-0.20858-0.26132-0.37209-0.28935-0.38361-0.0653-0.77121-0.0979-1.1588-0.0979zm0 4.1365a2.8152 2.8635 0 0 1 2.8152 2.8636 2.8152 2.8635 0 0 1-2.8152 2.8635 2.8152 2.8635 0 0 1-2.8152-2.8635 2.8152 2.8635 0 0 1 2.8152-2.8636z" fill="#fff" stroke-width=".25254"/></svg> | ||||
| After Width: | Height: | Size: 1.8 KiB | 
| Before Width: | Height: | Size: 880 B | 
							
								
								
									
										1
									
								
								portprotonqt/themes/standart/images/key_backspace.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g><rect x="1" y="6" width="46" height="36" rx="5" ry="5" fill="#3f424d" stroke-width="1.1506"/><rect x="4.2329" y="8.5301" width="39.534" height="30.94" rx="4.2972" ry="4.2972" fill="#fff" stroke-width=".98888"/><path d="m23.24 22.785c-0.67917 0.69059-0.67818 1.807 0 2.4913l8.0309 8.1037c1.8756 1.8787 4.6892-0.93962 2.8136-2.8183l-3.5038-3.5097c-0.58434-0.58533-0.39618-1.0598 0.44066-1.0598h9.6139c1.0992 0 1.9895-0.89179 1.9895-1.9928 0-1.1005-0.89028-1.9928-1.9895-1.9928h-9.6139c-0.82771 0-1.0277-0.47176-0.44066-1.0597l3.5038-3.5093c1.8756-1.8787-0.93803-4.6971-2.8136-2.8183z" fill="#3f424d" fill-rule="evenodd"/></g></svg> | ||||
| After Width: | Height: | Size: 751 B | 
| Before Width: | Height: | Size: 2.0 KiB | 
							
								
								
									
										48
									
								
								portprotonqt/themes/standart/images/key_context.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,48 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <svg | ||||
|    width="48" | ||||
|    height="48" | ||||
|    version="1.1" | ||||
|    viewBox="0 0 48 48" | ||||
|    xml:space="preserve" | ||||
|    id="svg2" | ||||
|    sodipodi:docname="key_context.svg" | ||||
|    inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)" | ||||
|    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | ||||
|    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | ||||
|    xmlns="http://www.w3.org/2000/svg" | ||||
|    xmlns:svg="http://www.w3.org/2000/svg"><defs | ||||
|      id="defs2" /><sodipodi:namedview | ||||
|      id="namedview2" | ||||
|      pagecolor="#ffffff" | ||||
|      bordercolor="#000000" | ||||
|      borderopacity="0.25" | ||||
|      inkscape:showpageshadow="2" | ||||
|      inkscape:pageopacity="0.0" | ||||
|      inkscape:pagecheckerboard="0" | ||||
|      inkscape:deskcolor="#d1d1d1" | ||||
|      inkscape:zoom="8.6915209" | ||||
|      inkscape:cx="72.311855" | ||||
|      inkscape:cy="22.780823" | ||||
|      inkscape:window-width="2560" | ||||
|      inkscape:window-height="1406" | ||||
|      inkscape:window-x="0" | ||||
|      inkscape:window-y="0" | ||||
|      inkscape:window-maximized="1" | ||||
|      inkscape:current-layer="svg2" /><path | ||||
|      style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:#686e7e;fill-opacity:1;stroke-width:0.554217;enable-background:accumulate;stop-color:#000000" | ||||
|      d="m 17.400964,38.281601 -0.04068,-15.381724 c -0.0087,-3.288656 2.401967,-6.020242 5.542168,-6.550475 V 7.4098472 C 11.174091,7.9874382 1.8422139,17.678792 1.8422139,29.550445 v 8.911269 c 3.429133,2.844892 11.5678151,2.890776 15.5587501,-0.180113 z" | ||||
|      id="path10" | ||||
|      sodipodi:nodetypes="csccscc" /><path | ||||
|      fill="#000000" | ||||
|      d="m 23.956256,40.5905 h -9e-6 c -2.438553,0 -4.433731,-1.995178 -4.433731,-4.43373 V 25.072424 c 0,-2.438552 1.995178,-4.433731 4.433731,-4.433731 h 9e-6 c 2.438552,0 4.43373,1.995179 4.43373,4.433731 V 36.15677 c 0,2.438552 -1.995178,4.43373 -4.43373,4.43373 z" | ||||
|      id="path2" | ||||
|      style="fill:#686e7e;fill-opacity:1;stroke-width:0.554217" /><g | ||||
|      id="g15" | ||||
|      transform="matrix(0.97480136,0,0,0.99852328,1.4840752,1.6593149)"><path | ||||
|        style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:#ffffff;stroke-width:0.95333;enable-background:accumulate;stop-color:#000000" | ||||
|        d="m 30.231637,35.990171 0.03878,-14.663865 c 0.0083,-3.135176 -2.289868,-5.73928 -5.283518,-6.244767 V 6.5591888 C 36.167905,7.1098239 45.209208,16.349815 45.064267,27.666494 l -0.109685,8.563937 c -3.269097,2.712122 -10.918265,2.687312 -14.722945,-0.24026 z" | ||||
|        id="path14" /><path | ||||
|        style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:#686e7e;fill-opacity:1;stroke-width:0.95333;enable-background:accumulate;stop-color:#000000" | ||||
|        d="m 24.224126,5.7586892 v 9.9671448 l 0.634933,0.107994 c 2.632815,0.444559 4.656653,2.729598 4.649348,5.490959 l -0.04096,15.03916 0.299778,0.230885 c 2.097287,1.613791 5.093143,2.357986 8.017658,2.392636 2.924514,0.03465 5.796042,-0.625772 7.656435,-2.169199 l 0.271848,-0.2253 0.113581,-8.91699 C 45.976953,15.94787 36.604257,6.3680498 25.024774,5.7977906 Z m 1.524956,1.6795 C 36.150995,8.3658717 44.437912,17.028984 44.301786,27.65736 l -0.104271,8.114479 c -1.445908,1.069255 -3.851487,1.720797 -6.394017,1.690673 -2.543438,-0.03013 -5.090881,-0.734663 -6.807375,-1.934591 l 0.03724,-14.199409 c 0.0087,-3.271088 -2.263607,-5.953645 -5.284281,-6.771998 z" | ||||
|        id="path15" /></g></svg> | ||||
| After Width: | Height: | Size: 3.3 KiB | 
| Before Width: | Height: | Size: 874 B | 
							
								
								
									
										1
									
								
								portprotonqt/themes/standart/images/key_e.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m12 6c-3.3238 0-5.9998 2.6763-5.9998 5.9998v24c0 3.3238 2.6763 5.9998 5.9998 5.9998h24c3.3238 0 5.9998-2.6763 5.9998-5.9998v-24c0-3.3238-2.6763-5.9998-5.9998-5.9998z" fill="#3f424d" stroke-width="2.5714"/><path d="m13.697 8.5452c-2.8537 0-5.1515 2.2979-5.1515 5.1515v20.606c0 2.8537 2.2979 5.1515 5.1515 5.1515h20.606c2.8537 0 5.1515-2.2979 5.1515-5.1515v-20.606c0-2.8537-2.2979-5.1515-5.1515-5.1515z" fill="#fff" stroke-width="2.2078"/><path d="m17.977 16.26h11.807v2.6476h-8.086v3.554h7.2989v2.6476h-7.2989v3.9834h8.3245v2.6476h-12.046z" fill="#3f424d" stroke-width=".4977" aria-label="E"/></svg> | ||||
| After Width: | Height: | Size: 726 B | 
| Before Width: | Height: | Size: 1.3 KiB | 
							
								
								
									
										1
									
								
								portprotonqt/themes/standart/images/key_enter.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m6 6h36c2.77 0 5 2.23 5 5v26c0 2.77-2.23 5-5 5h-36c-2.77 0-5-2.23-5-5v-26c0-2.77 2.23-5 5-5z" fill="#3f424d" stroke-width="1.1506"/><path d="m8.5301 8.5301h30.94c2.3806 0 4.2972 1.9166 4.2972 4.2972v22.346c0 2.3806-1.9166 4.2972-4.2972 4.2972h-30.94c-2.3806 0-4.2972-1.9166-4.2972-4.2972v-22.346c0-2.3806 1.9166-4.2972 4.2972-4.2972z" fill="#fff" stroke-width=".98888"/><path d="m8.2952 18.538h8.3321v1.8684h-5.7063v2.5081h5.1508v1.8684h-5.1508v2.811h5.8746v1.8684h-8.5005zm10.268 0h2.6596l5.2854 7.4568v-7.4568h2.3397v10.924h-2.6596l-5.2854-7.5747v7.5747h-2.3397zm15.166 1.8684h-3.3665v-1.8684h9.3421v1.8684h-3.3497v9.0559h-2.6259z" fill="#3f424d" stroke-width=".35123" aria-label="ENT"/></svg> | ||||
| After Width: | Height: | Size: 823 B | 
| Before Width: | Height: | Size: 943 B | 
							
								
								
									
										1
									
								
								portprotonqt/themes/standart/images/key_f11.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m12 6c-3.3238 0-5.9998 2.6763-5.9998 5.9998v24c0 3.3238 2.6763 5.9998 5.9998 5.9998h24c3.3238 0 5.9998-2.6763 5.9998-5.9998v-24c0-3.3238-2.6763-5.9998-5.9998-5.9998z" fill="#3f424d" stroke-width="2.5714"/><path d="m13.697 8.5452c-2.8537 0-5.1515 2.2979-5.1515 5.1515v20.606c0 2.8537 2.2979 5.1515 5.1515 5.1515h20.606c2.8537 0 5.1515-2.2979 5.1515-5.1515v-20.606c0-2.8537-2.2979-5.1515-5.1515-5.1515z" fill="#fff" stroke-width="2.2078"/><path d="m11.139 18.538h8.5005v1.8684h-5.8746v2.6764h5.3191v1.8684h-5.3191v4.5111h-2.6259zm13.5 2.5754-2.9794 1.6496v-2.0704l3.4507-2.1546h1.9862v10.924h-2.4576zm9.7629 0-2.9794 1.6496v-2.0704l3.4507-2.1546h1.9862v10.924h-2.4576z" fill="#3f424d" stroke-width=".35123" aria-label="F11"/></svg> | ||||
| After Width: | Height: | Size: 857 B | 
| Before Width: | Height: | Size: 933 B | 
							
								
								
									
										1
									
								
								portprotonqt/themes/standart/images/key_left.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:none;}</style></defs><path d="m12 6c-3.3238 0-5.9998 2.6763-5.9998 5.9998v24c0 3.3238 2.6763 5.9998 5.9998 5.9998h24c3.3238 0 5.9998-2.6763 5.9998-5.9998v-24c0-3.3238-2.6763-5.9998-5.9998-5.9998z" fill="#3f424d" stroke-width="2.5714"/><path d="m13.697 8.5452c-2.8537 0-5.1515 2.2979-5.1515 5.1515v20.606c0 2.8537 2.2979 5.1515 5.1515 5.1515h20.606c2.8537 0 5.1515-2.2979 5.1515-5.1515v-20.606c0-2.8537-2.2979-5.1515-5.1515-5.1515z" fill="#fff" stroke-width="2.2078"/><path d="m26.619 34a1.9874 1.9874 0 0 1-1.3812-0.55623l-7.5143-7.2497a3.0457 3.0457 0 0 1 0-4.3873l7.5143-7.2497a1.9882 1.9882 0 0 1 2.7603 2.8624l-6.8226 6.581 6.8226 6.581a1.9874 1.9874 0 0 1-1.3791 3.4186z" fill="#3f424d" stroke-width=".20832"/></svg> | ||||
| After Width: | Height: | Size: 865 B | 
| Before Width: | Height: | Size: 956 B | 
							
								
								
									
										1
									
								
								portprotonqt/themes/standart/images/key_right.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:none;}</style></defs><path d="m12 6c-3.3238 0-5.9998 2.6763-5.9998 5.9998v24c0 3.3238 2.6763 5.9998 5.9998 5.9998h24c3.3238 0 5.9998-2.6763 5.9998-5.9998v-24c0-3.3238-2.6763-5.9998-5.9998-5.9998z" fill="#3f424d" stroke-width="2.5714"/><path d="m13.697 8.5452c-2.8537 0-5.1515 2.2979-5.1515 5.1515v20.606c0 2.8537 2.2979 5.1515 5.1515 5.1515h20.606c2.8537 0 5.1515-2.2979 5.1515-5.1515v-20.606c0-2.8537-2.2979-5.1515-5.1515-5.1515z" fill="#fff" stroke-width="2.2078"/><path d="m20.778 34a1.9874 1.9874 0 0 0 1.3812-0.55623l7.5143-7.2497a3.0457 3.0457 0 0 0 0-4.3873l-7.5143-7.2497a1.9882 1.9882 0 0 0-2.7603 2.8624l6.8226 6.581-6.8226 6.581a1.9874 1.9874 0 0 0 1.3791 3.4186z" fill="#3f424d" stroke-width=".20832"/></svg> | ||||
| After Width: | Height: | Size: 864 B | 
| Before Width: | Height: | Size: 1.9 KiB | 
							
								
								
									
										1
									
								
								portprotonqt/themes/standart/images/ps_circle.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m24 13.476c-5.7918 0-10.524 4.7162-10.524 10.524 0 5.7918 4.7162 10.524 10.524 10.524 5.7918 0 10.524-4.7162 10.524-10.524 0-5.7918-4.7162-10.524-10.524-10.524zm0 18.037c-4.137 0-7.5128-3.3758-7.5128-7.5128s3.3758-7.5128 7.5128-7.5128 7.5128 3.3758 7.5128 7.5128-3.3592 7.5128-7.5128 7.5128z" fill="#3f424d" stroke-width="1.6548"/></svg> | ||||
| After Width: | Height: | Size: 736 B | 
| Before Width: | Height: | Size: 1.7 KiB | 
							
								
								
									
										1
									
								
								portprotonqt/themes/standart/images/ps_cross.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m34.076 13.91c-0.57906-0.57906-1.5387-0.57906-2.1177 0l-7.958 7.958-7.958-7.958c-0.57906-0.57906-1.5387-0.57906-2.1177 0-0.57906 0.57906-0.57906 1.5387 0 2.1177l7.958 7.958-7.958 7.958c-0.57906 0.57906-0.57906 1.5387 0 2.1177 0.2978 0.2978 0.67833 0.44671 1.0589 0.44671 0.38053 0 0.76106-0.1489 1.0589-0.44671l7.958-7.9415 7.958 7.958c0.2978 0.2978 0.67833 0.44671 1.0589 0.44671s0.76106-0.1489 1.0589-0.44671c0.57906-0.57906 0.57906-1.5387 0-2.1177l-7.958-7.958 7.958-7.958c0.57906-0.59561 0.57906-1.5387 0-2.1343z" fill="#3f424d" stroke-width="1.6545"/></svg> | ||||
| After Width: | Height: | Size: 961 B | 
| Before Width: | Height: | Size: 1.7 KiB | 
							
								
								
									
										1
									
								
								portprotonqt/themes/standart/images/ps_l1.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m10.465 39.437c4.1391 1.4258 20.596 4.9156 31.79 2.551 2.7034-0.57104 4.7508-3.32 4.744-6.0831l-0.057386-23.467c-0.009676-3.9677-4.6895-7.2319-7.5124-7.2255-12.075 0.0276-22.278-0.0068827-33.557 1.5493-2.7371 0.37765-4.8753 4.0033-4.8727 6.7663l0.016807 17.988c0.00451 4.8315 6.0288 6.743 9.4485 7.921z" fill="#3f424d" stroke-width="1.1477"/><path d="m12.394 37.236c3.5492 1.2226 17.661 4.2149 27.259 2.1874 2.3181-0.48964 4.0736-2.8468 4.0678-5.216l-0.049207-20.123c-0.008279-3.4022-4.0211-6.2011-6.4416-6.1956-10.354 0.023666-19.103-0.0059052-28.774 1.3285-2.347 0.32383-4.1804 3.4327-4.1782 5.802l0.014412 15.424c0.00387 4.1428 5.1694 5.7819 8.1017 6.792z" fill="#fff" stroke-width=".98413"/><path d="m13.833 16.812h3.4556v11.917h7.0662v2.4588h-10.522zm17.101 3.3891-3.9207 2.1708v-2.7246l4.541-2.8353h2.6138v14.376h-3.234z" fill="#3f424d" stroke-width=".4622" aria-label="L1"/></svg> | ||||
| After Width: | Height: | Size: 1015 B | 
| Before Width: | Height: | Size: 1.3 KiB | 
							
								
								
									
										1
									
								
								portprotonqt/themes/standart/images/ps_options.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m18.047 46.216-2.1e-5 -5e-6c-5.4306-1.4551-8.6833-7.089-7.2282-12.52l6.6143-24.685c1.4551-5.4306 7.089-8.6833 12.52-7.2282l2.1e-5 5.5e-6c5.4306 1.4551 8.6833 7.089 7.2282 12.52l-6.6143 24.685c-1.4551 5.4306-7.089 8.6833-12.52 7.2282z" fill="#3f424d" stroke-width="1.2778"/><path d="m19.229 41.807-1.7e-5 -4e-6c-4.3529-1.1664-6.9601-5.6821-5.7937-10.035l5.3016-19.786c1.1664-4.3529 5.6821-6.9601 10.035-5.7937l1.7e-5 4.4e-6c4.3529 1.1664 6.9601 5.6821 5.7937 10.035l-5.3016 19.786c-1.1664 4.3529-5.6821 6.9601-10.035 5.7937z" fill="#fff" stroke-width="1.0242"/><path d="m19.502 18.291c-0.854 0-1.547 0.49867-1.547 1.114 0 0.6153 0.69299 1.114 1.547 1.114h8.997c0.854 0 1.5459-0.49867 1.5459-1.114 0-0.6153-0.69187-1.114-1.5459-1.114zm0 4.595c-0.854 0-1.547 0.49867-1.547 1.114 0 0.6153 0.69299 1.114 1.547 1.114h8.997c0.854 0 1.5459-0.49867 1.5459-1.114 0-0.6153-0.69187-1.114-1.5459-1.114zm0 4.595c-0.854 0-1.547 0.49867-1.547 1.114s0.69299 1.114 1.547 1.114h8.997c0.854 0 1.5459-0.49867 1.5459-1.114s-0.69187-1.114-1.5459-1.114z" fill="#3f424d" fill-rule="evenodd" stroke-width=".11455"/></svg> | ||||
| After Width: | Height: | Size: 1.2 KiB | 
| Before Width: | Height: | Size: 1.8 KiB | 
							
								
								
									
										1
									
								
								portprotonqt/themes/standart/images/ps_r1.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m37.535 39.437c-4.1391 1.4258-20.596 4.9156-31.79 2.551-2.7034-0.57104-4.7508-3.32-4.744-6.0831l0.057386-23.467c0.00968-3.9677 4.6895-7.2319 7.5124-7.2255 12.075 0.0276 22.278-0.00688 33.557 1.5493 2.7371 0.37765 4.8753 4.0033 4.8727 6.7663l-0.01681 17.988c-0.0045 4.8315-6.0288 6.743-9.4485 7.921z" fill="#3f424d" stroke-width="1.1477"/><path d="m35.606 37.236c-3.5492 1.2226-17.661 4.2149-27.259 2.1874-2.3181-0.48964-4.0736-2.8468-4.0678-5.216l0.049207-20.123c0.00828-3.4022 4.0211-6.2011 6.4416-6.1956 10.354 0.023666 19.103-0.00591 28.774 1.3285 2.347 0.32383 4.1804 3.4327 4.1782 5.802l-0.01441 15.424c-0.0039 4.1428-5.1694 5.7819-8.1017 6.792z" fill="#fff" stroke-width=".98413"/><path d="m12.858 16.812h6.4681q2.8796 0 4.1644 0.70883 1.2848 0.68668 1.2848 2.3259v2.5252q0 1.2626-0.90819 1.9936-0.88604 0.70883-2.3702 0.90819l4.1644 5.9143h-3.9872l-3.7657-5.6485h-1.5949v5.6485h-3.4556zm6.4238 6.4459q1.2183 0 1.6613-0.31011 0.44302-0.33226 0.44302-1.2626v-1.0189q0-0.79744-0.48732-1.0854-0.46517-0.31011-1.617-0.31011h-2.9682v3.9872zm12.626-3.0568-3.9207 2.1708v-2.7246l4.541-2.8353h2.6138v14.376h-3.234z" fill="#3f424d" stroke-width=".4622" aria-label="R1"/></svg> | ||||
| After Width: | Height: | Size: 1.3 KiB | 
| Before Width: | Height: | Size: 1.4 KiB | 
							
								
								
									
										1
									
								
								portprotonqt/themes/standart/images/ps_share.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m29.953 46.216 2.1e-5 -5e-6c5.4306-1.4551 8.6833-7.089 7.2282-12.52l-6.6143-24.685c-1.4551-5.4306-7.089-8.6833-12.52-7.2282l-2.1e-5 5.5e-6c-5.4306 1.4551-8.6833 7.089-7.2282 12.52l6.6143 24.685c1.4551 5.4306 7.089 8.6833 12.52 7.2282z" fill="#3f424d" stroke-width="1.2778"/><path d="m28.771 41.807 1.7e-5 -4e-6c4.3529-1.1664 6.9601-5.6821 5.7937-10.035l-5.3016-19.786c-1.1664-4.3529-5.6821-6.9601-10.035-5.7937l-1.7e-5 4.4e-6c-4.3529 1.1664-6.9601 5.6821-5.7937 10.035l5.3016 19.786c1.1664 4.3529 5.6821 6.9601 10.035 5.7937z" fill="#fff" stroke-width="1.0242"/><path d="m24.034 20.416c-0.54232 0-0.98296 0.41005-0.98296 0.91636v5.3348c0 0.50632 0.44064 0.91636 0.98296 0.91636s0.98124-0.41005 0.98124-0.91636v-5.3348c0-0.50632-0.43892-0.91636-0.98124-0.91636zm-5.9615 0.72033c-0.15955 0.0017-0.31975 0.03855-0.46652 0.11513-0.46966 0.24506-0.62269 0.79993-0.34257 1.2384l2.9506 4.6191c0.28012 0.43848 0.88858 0.59512 1.3582 0.35005 0.46966-0.24506 0.62269-0.79837 0.34257-1.2369l-2.9506-4.6192c-0.19258-0.30146-0.5407-0.4705-0.89172-0.46674zm11.856 0c-0.35102-0.0037-0.69914 0.16528-0.89172 0.46674l-2.9506 4.6191c-0.28011 0.43848-0.12709 0.99179 0.34257 1.2369 0.46967 0.24506 1.0781 0.08843 1.3582-0.35005l2.9506-4.6191c0.28011-0.43848 0.12709-0.99335-0.34257-1.2384-0.14677-0.07658-0.30696-0.11342-0.46652-0.11513z" fill="#3f424d" fill-rule="evenodd" stroke-width=".082805"/></svg> | ||||
| After Width: | Height: | Size: 1.5 KiB | 
							
								
								
									
										1
									
								
								portprotonqt/themes/standart/images/ps_square.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m33.019 13.476h-18.037c-0.8274 0-1.5059 0.67847-1.5059 1.5059v18.037c0 0.8274 0.67847 1.5059 1.5059 1.5059h18.037c0.8274 0 1.5059-0.67847 1.5059-1.5059v-18.037c0-0.8274-0.66192-1.5059-1.5059-1.5059zm-1.4893 18.037h-15.026v-15.026h15.026z" fill="#3f424d" stroke-width="1.6548"/></svg> | ||||
| After Width: | Height: | Size: 682 B | 
| Before Width: | Height: | Size: 1.8 KiB | 
							
								
								
									
										1
									
								
								portprotonqt/themes/standart/images/ps_triangle.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m13.766 32.511h20.449c0.60033 0 1.1631-0.31892 1.4821-0.84421 0.30016-0.52529 0.30016-1.1819 0-1.7072l-10.224-17.71c-0.60033-1.0506-2.345-1.0506-2.9454 0l-10.224 17.71c-0.30016 0.52529-0.30016 1.1819 0 1.7072s0.86297 0.84421 1.4633 0.84421zm10.224-15.984 7.2602 12.588h-14.539z" fill="#3f424d" stroke-width="1.876"/></svg> | ||||
| After Width: | Height: | Size: 721 B | 
| After Width: | Height: | Size: 232 KiB | 
							
								
								
									
										
											BIN
										
									
								
								portprotonqt/themes/standart/images/screenshots/Библиотека.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 225 KiB | 
| Before Width: | Height: | Size: 1.3 MiB | 
							
								
								
									
										
											BIN
										
									
								
								portprotonqt/themes/standart/images/screenshots/Карточка.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 70 KiB | 
| Before Width: | Height: | Size: 364 KiB | 
| Before Width: | Height: | Size: 430 KiB | 
| After Width: | Height: | Size: 238 KiB | 
| Before Width: | Height: | Size: 1.3 MiB | 
| After Width: | Height: | Size: 61 KiB | 
| After Width: | Height: | Size: 38 KiB | 
| Before Width: | Height: | Size: 104 KiB | 
| Before Width: | Height: | Size: 1.0 MiB | 
							
								
								
									
										
											BIN
										
									
								
								portprotonqt/themes/standart/images/screenshots/Темы.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 93 KiB | 
| Before Width: | Height: | Size: 1.7 KiB | 
							
								
								
									
										1
									
								
								portprotonqt/themes/standart/images/xbox_a.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m21.016 13.475h6.1623l7.5893 21.049h-5.1244l-1.8811-5.546h-7.6866l-1.8487 5.546h-4.9947zm5.6433 12.13-2.6595-7.9137h-0.12973l-2.6595 7.9137z" fill="#3f424d" stroke-width=".67675" aria-label="A"/></svg> | ||||
| After Width: | Height: | Size: 600 B | 
| Before Width: | Height: | Size: 1.7 KiB | 
							
								
								
									
										1
									
								
								portprotonqt/themes/standart/images/xbox_b.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m15.973 13.476h8.5299q3.0163 0 4.6379 0.45406 1.6541 0.42163 2.3352 1.3946 0.68109 0.94056 0.68109 2.6595v2.5946q0 0.87569-0.71353 1.6541-0.68109 0.77839-1.6216 1.0703v0.16216q1.2325 0.12973 2.2379 1.0703 1.0379 0.90812 1.0379 2.0433v3.2433q0 2.5622-2.0433 3.6325t-6.3244 1.0703h-8.7569zm8.5299 8.5623q1.2 0 1.7838-0.1946t0.77839-0.61623q0.22703-0.45406 0.22703-1.2973v-1.0379q0-0.74596-0.1946-1.1027-0.1946-0.3892-0.81082-0.55136-0.58379-0.16216-1.8811-0.16216h-3.373v4.9622zm0.12973 8.8866q1.8487 0 2.6271-0.42163t0.77839-1.3622v-1.6865q0-1.1676-0.61623-1.6541-0.58379-0.4865-2.1081-0.4865h-4.2812v5.6109z" fill="#3f424d" stroke-width=".67675" aria-label="B"/></svg> | ||||
| After Width: | Height: | Size: 1.0 KiB | 
| Before Width: | Height: | Size: 1.9 KiB | 
							
								
								
									
										1
									
								
								portprotonqt/themes/standart/images/xbox_lb.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m6.3645 12.915h35.271c2.9719 0 5.3645 2.3926 5.3645 5.3645v11.442c0 2.9719-2.3926 5.3645-5.3645 5.3645h-35.271c-2.9719 0-5.3645-2.3926-5.3645-5.3645v-11.442c0-2.9719 2.3926-5.3645 5.3645-5.3645z" fill="#3f424d"/><path d="m9.2118 14.704h29.576c2.4921 0 4.4983 2.0062 4.4983 4.4983v9.5944c0 2.4921-2.0062 4.4983-4.4983 4.4983h-29.576c-2.4921 0-4.4983-2.0062-4.4983-4.4983v-9.5944c0-2.4921 2.0062-4.4983 4.4983-4.4983z" fill="#fff"/><path d="m13.757 18h2.8844v9.9476h5.8983v2.0524h-8.7827zm10.724 0h4.8629q1.7196 0 2.6441 0.25886 0.94299 0.24037 1.3313 0.79507 0.38829 0.53621 0.38829 1.5162v1.4792q0 0.49923-0.40678 0.94299-0.38829 0.44376-0.9245 0.61017v0.09245q0.70262 0.07396 1.2758 0.61017 0.59168 0.51772 0.59168 1.1649v1.849q0 1.4607-1.1649 2.0709-1.1649 0.61017-3.6055 0.61017h-4.9923zm4.8629 4.8814q0.68413 0 1.0169-0.11094 0.33282-0.11094 0.44376-0.35131 0.12943-0.25886 0.12943-0.7396v-0.59168q0-0.42527-0.11094-0.62866-0.11094-0.22188-0.46225-0.31433-0.33282-0.09245-1.0724-0.09245h-1.923v2.829zm0.07396 5.0663q1.0539 0 1.4977-0.24037 0.44376-0.24037 0.44376-0.77658v-0.96148q0-0.66564-0.35131-0.94299-0.33282-0.27735-1.2018-0.27735h-2.4407v3.1988z" fill="#3f424d" stroke-width=".38581" aria-label="LB"/></svg> | ||||
| After Width: | Height: | Size: 1.3 KiB |