Compare commits
67 Commits
599644c4f6
...
main
Author | SHA1 | Date | |
---|---|---|---|
0294c90c54
|
|||
17dfef2d27
|
|||
|
f0690f8811
|
||
ac20447ba3
|
|||
ba143c15a8
|
|||
13068f3959
|
|||
|
c8360d08ca | ||
b070ff1fca
|
|||
b5a2f41bdf
|
|||
9a37f31841
|
|||
aeed0112cd
|
|||
027ae68d4d
|
|||
37d41fef8d
|
|||
e37422fc95
|
|||
d7951e8587
|
|||
556533785a
|
|||
a13aca4d84
|
|||
35736e1723
|
|||
|
24a7c2e657
|
||
|
279f7ec36b
|
||
41f6943998
|
|||
3bf10dc4cd
|
|||
33b96d3185
|
|||
3573b8e373
|
|||
582ddd2218
|
|||
2753e53a4d
|
|||
46973f35e1
|
|||
8e34c92385
|
|||
d50b63bca7
|
|||
6966253e9b
|
|||
13f3af7a42
|
|||
c7bed80570
|
|||
6fde7c18db
|
|||
37782d4375
|
|||
0a8a7c538c
|
|||
|
9cc4b8c51d | ||
397dede2be
|
|||
6a66f37ba1
|
|||
4db1cce32c
|
|||
edaeca4f11
|
|||
11d44f091d
|
|||
09d9c6510a
|
|||
272be51bb0
|
|||
63933172f9
|
|||
85e9aba836
|
|||
4d3499d2c1
|
|||
a13c15bc28
|
|||
83076d3dfc
|
|||
04aaf68e36
|
|||
e91037708a
|
|||
1b743026c2
|
|||
30b4cec4d1
|
|||
db68c9050c
|
|||
1a93d5b82c
|
|||
cc0690cf9e
|
|||
809ba2c976
|
|||
68c9636e10
|
|||
f0df1f89be
|
|||
f25224b668
|
|||
0cda47fdfd
|
|||
1a8c733580
|
|||
2476bea32a
|
|||
1bbc95a5c1
|
|||
d12b801191
|
|||
233dab1269
|
|||
700a478598
|
|||
0fe727331f
|
@@ -17,10 +17,12 @@ jobs:
|
||||
- name: Install required dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y binutils coreutils desktop-file-utils gtk-update-icon-cache fakeroot fuse libgdk-pixbuf2.0-dev patchelf python3-pip python3-dev python3-setuptools squashfs-tools strace util-linux zsync
|
||||
sudo apt install -y binutils coreutils desktop-file-utils gtk-update-icon-cache fakeroot fuse libgdk-pixbuf2.0-dev patchelf python3-pip python3-dev python3-setuptools squashfs-tools strace util-linux zsync git zstd
|
||||
|
||||
- name: Install tools
|
||||
run: pip3 install appimage-builder uv
|
||||
run: |
|
||||
pip3 install git+https://github.com/Boria138/appimage-builder.git
|
||||
pip3 install uv
|
||||
|
||||
- name: Build AppImage
|
||||
run: |
|
||||
|
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
env:
|
||||
# Common version, will be used for tagging the release
|
||||
VERSION: 0.1.3
|
||||
VERSION: 0.1.4
|
||||
PKGDEST: "/tmp/portprotonqt"
|
||||
PACKAGE: "portprotonqt"
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
@@ -23,10 +23,12 @@ jobs:
|
||||
- name: Install required dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y binutils coreutils desktop-file-utils gtk-update-icon-cache fakeroot fuse libgdk-pixbuf2.0-dev patchelf python3-pip python3-dev python3-setuptools squashfs-tools strace util-linux zsync
|
||||
sudo apt install -y binutils coreutils desktop-file-utils gtk-update-icon-cache fakeroot fuse libgdk-pixbuf2.0-dev patchelf python3-pip python3-dev python3-setuptools squashfs-tools strace util-linux zsync git zstd
|
||||
|
||||
- name: Install tools
|
||||
run: pip3 install appimage-builder uv
|
||||
run: |
|
||||
pip3 install git+https://github.com/Boria138/appimage-builder.git
|
||||
pip3 install uv
|
||||
|
||||
- name: Build AppImage
|
||||
run: |
|
||||
@@ -157,6 +159,7 @@ jobs:
|
||||
mkdir -p extracted
|
||||
find release/ -name '*.zip' -exec unzip -o {} -d extracted/ \;
|
||||
find extracted/ -type f -exec mv {} release/ \;
|
||||
find release/ -name '*.zip' -delete
|
||||
rm -rf extracted/
|
||||
|
||||
- name: Extract changelog for version
|
||||
|
187
.gitea/workflows/code-build.yml
Normal file
@@ -0,0 +1,187 @@
|
||||
name: Build Check - AppImage, Arch, Fedora
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'build-aux/**'
|
||||
|
||||
env:
|
||||
PKGDEST: "/tmp/portprotonqt"
|
||||
PACKAGE: "portprotonqt"
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
appimage: ${{ steps.check.outputs.appimage }}
|
||||
fedora: ${{ steps.check.outputs.fedora }}
|
||||
arch: ${{ steps.check.outputs.arch }}
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Ensure git is installed
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y git
|
||||
|
||||
- name: Check changed files
|
||||
id: check
|
||||
run: |
|
||||
# Get changed files
|
||||
git diff --name-only ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} > changed_files.txt
|
||||
|
||||
echo "Changed files:"
|
||||
cat changed_files.txt
|
||||
|
||||
# Check AppImage files
|
||||
if grep -q "build-aux/AppImageBuilder.yml" changed_files.txt; then
|
||||
echo "appimage=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "appimage=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Check Fedora spec files (only fedora-git.spec)
|
||||
if grep -q "build-aux/fedora-git.spec" changed_files.txt; then
|
||||
echo "fedora=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "fedora=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Check Arch PKGBUILD-git
|
||||
if grep -q "build-aux/PKGBUILD-git" changed_files.txt; then
|
||||
echo "arch=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "arch=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
build-appimage:
|
||||
name: Build AppImage
|
||||
runs-on: ubuntu-22.04
|
||||
needs: changes
|
||||
if: needs.changes.outputs.appimage == 'true' || github.event_name == 'workflow_dispatch'
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@v4
|
||||
|
||||
- name: Install required dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y binutils coreutils desktop-file-utils gtk-update-icon-cache fakeroot fuse libgdk-pixbuf2.0-dev patchelf python3-pip python3-dev python3-setuptools squashfs-tools strace util-linux zsync zstd git
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
pip3 install git+https://github.com/Boria138/appimage-builder.git
|
||||
pip3 install uv
|
||||
|
||||
- name: Build AppImage
|
||||
run: |
|
||||
cd build-aux
|
||||
appimage-builder
|
||||
|
||||
- name: Upload AppImage
|
||||
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
||||
with:
|
||||
name: PortProtonQt-AppImage
|
||||
path: build-aux/PortProtonQt*.AppImage
|
||||
|
||||
build-fedora:
|
||||
name: Build Fedora RPM
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: needs.changes.outputs.fedora == 'true' || github.event_name == 'workflow_dispatch'
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
fedora_version: [41, 42, rawhide]
|
||||
|
||||
container:
|
||||
image: fedora:${{ matrix.fedora_version }}
|
||||
options: --privileged
|
||||
|
||||
steps:
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
dnf install -y git rpmdevtools python3-devel python3-wheel python3-pip \
|
||||
python3-build pyproject-rpm-macros python3-setuptools \
|
||||
redhat-rpm-config nodejs npm
|
||||
|
||||
- name: Setup rpmbuild environment
|
||||
run: |
|
||||
useradd rpmbuild -u 5002 -g users || true
|
||||
mkdir -p /home/rpmbuild/{BUILD,RPMS,SPECS,SRPMS,SOURCES}
|
||||
chown -R rpmbuild:users /home/rpmbuild
|
||||
echo '%_topdir /home/rpmbuild' > /home/rpmbuild/.rpmmacros
|
||||
|
||||
- name: Checkout repo
|
||||
uses: https://gitea.com/actions/checkout@v4
|
||||
|
||||
- name: Copy fedora-git.spec
|
||||
run: |
|
||||
cp build-aux/fedora-git.spec /home/rpmbuild/SPECS/${{ env.PACKAGE }}.spec
|
||||
chown -R rpmbuild:users /home/rpmbuild
|
||||
|
||||
- name: Build RPM
|
||||
run: |
|
||||
su rpmbuild -c "rpmbuild -ba /home/rpmbuild/SPECS/${{ env.PACKAGE }}.spec"
|
||||
|
||||
- name: Upload RPM package
|
||||
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
||||
with:
|
||||
name: PortProtonQt-RPM-Fedora-${{ matrix.fedora_version }}
|
||||
path: /home/rpmbuild/RPMS/**/*.rpm
|
||||
|
||||
build-arch:
|
||||
name: Build Arch Package
|
||||
runs-on: ubuntu-22.04
|
||||
needs: changes
|
||||
if: needs.changes.outputs.arch == 'true' || github.event_name == 'workflow_dispatch'
|
||||
container:
|
||||
image: archlinux:base-devel
|
||||
volumes:
|
||||
- /usr:/usr-host
|
||||
- /opt:/opt-host
|
||||
options: --privileged
|
||||
|
||||
steps:
|
||||
- name: Prepare container
|
||||
run: |
|
||||
pacman -Sy --noconfirm --disable-download-timeout --needed git wget gnupg nodejs npm
|
||||
sed -i 's/#MAKEFLAGS="-j2"/MAKEFLAGS="-j$(nproc) -l$(nproc)"/g' /etc/makepkg.conf
|
||||
sed -i 's/OPTIONS=(.*)/OPTIONS=(strip docs !libtool !staticlibs emptydirs zipman purge lto)/g' /etc/makepkg.conf
|
||||
yes | pacman -Scc
|
||||
pacman-key --init
|
||||
pacman -S --noconfirm archlinux-keyring
|
||||
mkdir -p /__w/portproton-repo
|
||||
pacman-key --recv-key 3056513887B78AEB --keyserver keyserver.ubuntu.com
|
||||
pacman-key --lsign-key 3056513887B78AEB
|
||||
pacman -U --noconfirm 'https://cdn-mirror.chaotic.cx/chaotic-aur/chaotic-keyring.pkg.tar.zst'
|
||||
pacman -U --noconfirm 'https://cdn-mirror.chaotic.cx/chaotic-aur/chaotic-mirrorlist.pkg.tar.zst'
|
||||
cat << EOM >> /etc/pacman.conf
|
||||
|
||||
[chaotic-aur]
|
||||
Include = /etc/pacman.d/chaotic-mirrorlist
|
||||
EOM
|
||||
pacman -Syy
|
||||
useradd -m user -G wheel && echo "user ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
|
||||
echo "PACKAGER=\"Boris Yumankulov <boria138@altlinux.org>\"" >> /etc/makepkg.conf
|
||||
chown user -R /tmp
|
||||
chown user -R ..
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cd /__w/portproton-repo
|
||||
git clone https://git.linux-gaming.ru/Boria138/PortProtonQt.git
|
||||
cd /__w/portproton-repo/PortProtonQt/build-aux
|
||||
chown user -R ..
|
||||
su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git"
|
||||
|
||||
- name: Checkout
|
||||
uses: https://gitea.com/actions/checkout@v4
|
||||
|
||||
- name: Upload Arch package
|
||||
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
||||
with:
|
||||
name: PortProtonQt-Arch
|
||||
path: ${{ env.PKGDEST }}/*
|
@@ -1,4 +1,4 @@
|
||||
name: Code and build check
|
||||
name: Code check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -35,20 +35,3 @@ jobs:
|
||||
run: |
|
||||
source .venv/bin/activate
|
||||
pre-commit run --show-diff-on-failure --color=always --all-files
|
||||
|
||||
build-uv:
|
||||
name: Build with uv
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: https://github.com/astral-sh/setup-uv@v6
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Sync dependencies
|
||||
run: uv sync
|
||||
|
||||
- name: Build project
|
||||
run: uv build
|
||||
|
@@ -8,11 +8,24 @@ on:
|
||||
jobs:
|
||||
renovate:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/renovatebot/renovate:41.1.4
|
||||
container: ghcr.io/renovatebot/renovate:latest
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@v4
|
||||
- run: renovate
|
||||
|
||||
- name: Install uv
|
||||
uses: https://github.com/astral-sh/setup-uv@v6
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Download external renovate config
|
||||
run: |
|
||||
mkdir -p /tmp/renovate-config
|
||||
curl -fsSL "https://git.linux-gaming.ru/Linux-Gaming/renovate-config/raw/branch/main/config.js" \
|
||||
-o /tmp/renovate-config/config.js
|
||||
|
||||
- name: Run Renovate
|
||||
run: renovate
|
||||
env:
|
||||
RENOVATE_CONFIG_FILE: "/workspace/Boria138/PortProtonQt/config.js"
|
||||
RENOVATE_CONFIG_FILE: "/tmp/renovate-config/config.js"
|
||||
LOG_LEVEL: "debug"
|
||||
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
||||
|
@@ -3,7 +3,7 @@
|
||||
exclude: '(data/|documentation/|portprotonqt/locales/|portprotonqt/custom_data/|dev-scripts/|\.venv/|venv/|.*\.svg$)'
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
@@ -11,15 +11,14 @@ repos:
|
||||
- id: check-yaml
|
||||
|
||||
- repo: https://github.com/astral-sh/uv-pre-commit
|
||||
rev: 0.6.14
|
||||
rev: 0.8.9
|
||||
hooks:
|
||||
- id: uv-lock
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.11.5
|
||||
rev: v0.12.8
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
- id: ruff-check
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
|
34
CHANGELOG.md
@@ -5,18 +5,52 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Больше типов анимаций при открытии карточки игры (за подробностями в документацию)
|
||||
- Анимация при выходе из карточки игры (за подробностями в документацию)
|
||||
|
||||
### Changed
|
||||
- Уменьшена длительность анимации открытия карточки с 800 до 350мс
|
||||
- Контекстное меню при открытие теперь сразу фокусируется на первом элементе
|
||||
- Анимации теперь можно настраивать через темы (за подробностями в документацию)
|
||||
- Общие json (steam_apps и anticheat_games) теперь перекачиваются если сломаны
|
||||
- Временно удалена светлая тема
|
||||
- Добавление и удаление игр из Steam теперь не требует перезагрузки Steam
|
||||
- Обновлены все зависимости (затрагивает только AppImage)
|
||||
- Удалён отдельный трей так как у PortProton есть свой трей
|
||||
|
||||
### Fixed
|
||||
- legendary list теперь не вызывается если вход в EGS не был произведён
|
||||
- Скриншоты тем теперь не теряют в качестве при масштабе отличном от 100%
|
||||
- Данные от HLTB теперь не отображаются в карточке если нет данных о времени прохождения
|
||||
- Диалог добавления игры теперь не добавляет игру если exe не существует
|
||||
|
||||
### Contributors
|
||||
- @Alex Smith
|
||||
|
||||
---
|
||||
|
||||
## [0.1.4] - 2025-07-21
|
||||
|
||||
### Added
|
||||
- Переводы в переопределениях (за подробностями в документацию)
|
||||
- Обложки и описания для всех автоинсталлов
|
||||
- Возможность указать ссылку для скачивания обложки в диалоге добавления игры
|
||||
- Интеграция с howlongtobeat.com
|
||||
|
||||
### Changed
|
||||
- Оптимизированны обложки автоинсталлов
|
||||
- Папка custom_data исключена из сборки модуля для уменьшение его размера
|
||||
- Бейдж PortProton теперь открывает PortProtonDB
|
||||
- Отключено переключение полноэкранного режима через F11 или кнопку Select на геймпаде в gamescope сессии
|
||||
- Удалён аргумент `--session` так как тестирование gamescope сессии завершено
|
||||
- В контекстном меню игр без exe файла теперь отображается только пункт "Удалить из PortProton"
|
||||
|
||||
### Fixed
|
||||
- Запрос к GitHub API при загрузке legendary теперь не игнорирует настройки прокси
|
||||
- Путь к portprotonqt-session-select в оверлее
|
||||
- Работа exiftool в AppImage
|
||||
- Открытие контекстного меню у игр без exe
|
||||
|
||||
### Contributors
|
||||
- @Vector_null
|
||||
|
13
LICENSE
@@ -73,6 +73,19 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
===============================
|
||||
= HowLongToBeat-Python-API : =
|
||||
===============================
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 JaeguKim
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
==============
|
||||
= legendary: =
|
||||
==============
|
||||
|
@@ -51,11 +51,11 @@ pre-commit run --all-files
|
||||
|
||||
PortProtonQt использует код и зависимости от следующих проектов:
|
||||
|
||||
- [Legendary](https://github.com/derrod/legendary) — инструмент для работы с Epic Games Store, лицензия [GPL-3.0](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||
- [Icoextract](https://github.com/jlu5/icoextract) — библиотека для извлечения иконок, лицензия [MIT](https://opensource.org/licenses/MIT).
|
||||
- [PortProton 2.0](https://git.linux-gaming.ru/CastroFidel/PortProton_2.0) — библиотека для взаимодействия с PortProton, лицензия [MIT](https://opensource.org/licenses/MIT).
|
||||
- [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).
|
||||
|
||||
Полный текст лицензий см. в файлах [LICENSE](LICENSE), [LICENSE-icoextract](documentation/licenses/icoextract), [LICENSE-portproton](documentation/licenses/portproton), [LICENSE-legendary](documentation/licenses/legendary).
|
||||
Полный текст лицензий см. в файле [LICENSE](LICENSE).
|
||||
|
||||
> [!WARNING]
|
||||
> Проект находится на стадии WIP (work in progress) корректная работоспособность не гарантирована
|
||||
|
5
TODO.md
@@ -17,7 +17,6 @@
|
||||
- [ ] Убрать логи Steam API в релизной версии, так как они замедляют выполнение кода
|
||||
- [X] Решить проблему с ограничением Steam API в 50 тысяч игр за один запрос (иногда нужные игры не попадают в выборку и остаются без обложки)
|
||||
- [X] Избавиться от вызовов yad
|
||||
- [X] Реализовать собственный механизм запрета ухода в спящий режим вместо использования механизма PortProton (оставлено для [PortProton 2.0](https://github.com/Castro-Fidel/PortProton_2.0))
|
||||
- [X] Реализовать собственный системный трей вместо использования трея PortProton
|
||||
- [X] Добавить экранную клавиатуру в поиск (реализация собственной клавиатуры слишком затратна, поэтому используется встроенная в DE клавиатура: Maliit в KDE, gjs-osk в GNOME, Squeekboard в Phosh, клавиатура SteamOS и т.д.)
|
||||
- [X] Добавить сортировку карточек по различным критериям (доступны: по недавности, количеству наигранного времени, избранному или алфавиту)
|
||||
@@ -42,6 +41,7 @@
|
||||
- [X] Получать slug через GraphQL [запрос](https://launcher.store.epicgames.com/graphql)
|
||||
- [X] Добавить на карточку бейдж, указывающий, что игра из Steam
|
||||
- [X] Добавить поддержку версий Steam для Flatpak и Snap
|
||||
- [X] Реализовать добавление игры как сторонней в Steam без перезапуска
|
||||
- [X] Отображать данные о самом последнем пользователе Steam, а не первом попавшемся
|
||||
- [X] Исправить склонения в детальном выводе времени, например, не «3 часов назад», а «3 часа назад»
|
||||
- [X] Добавить перевод через gettext [Документация](documentation/localization_guide)
|
||||
@@ -57,13 +57,12 @@
|
||||
- [X] Исправить наложение подписей скриншотов при первом перелистывании в полноэкранном режиме
|
||||
- [ ] Добавить поддержку GOG (?)
|
||||
- [X] Определиться с названием (PortProtonQt или PortProtonQT или вообще третий вариант)
|
||||
- [ ] Добавить данные с HowLongToBeat на страницу с деталями игры (?)
|
||||
- [X] Добавить данные с HowLongToBeat на страницу с деталями игры
|
||||
- [X] Добавить виброотдачу на геймпаде при запуске игры
|
||||
- [X] Исправить некорректную работу слайдера увеличения размера карточек([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63)
|
||||
- [X] Исправить баг с наложением карточек друг на друга при изменении фильтра отображения ([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63))
|
||||
- [X] Скопировать логику управления с D-pad на стрелки с клавиатуры
|
||||
- [ ] Доделать светлую тему
|
||||
- [ ] Добавить подсказки к управлению с геймпада
|
||||
- [ ] Добавить загрузку звуков в темы например для добавления звука запуска в тему и тд
|
||||
- [X] Добавить миниатюры к выбору файлов в диалоге добавления игры
|
||||
- [X] Добавить быстрый доступ к смонтированным дискам к выбору файлов в диалоге добавления игры
|
||||
|
@@ -1,5 +1,4 @@
|
||||
version: 1
|
||||
|
||||
script:
|
||||
# 1) чистим старый AppDir
|
||||
- rm -rf AppDir || true
|
||||
@@ -14,29 +13,48 @@ script:
|
||||
# 5) чистим от ненужных модулей и бинарников
|
||||
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/qml/
|
||||
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{assistant,designer,linguist,lrelease,lupdate}
|
||||
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{Qt3D*,QtBluetooth*,QtCharts*,QtConcurrent*,QtDataVisualization*,QtDesigner*,QtHelp*,QtMultimedia*,QtNetwork*,QtOpenGL*,QtPositioning*,QtPrintSupport*,QtQml*,QtQuick*,QtRemoteObjects*,QtScxml*,QtSensors*,QtSerialPort*,QtSql*,QtStateMachine*,QtTest*,QtWeb*,QtXml*}
|
||||
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{Qt3DAnimation*,Qt3DCore*,Qt3DExtras*,Qt3DInput*,Qt3DLogic*,Qt3DRender*,QtBluetooth*,QtCharts*,QtConcurrent*,QtDataVisualization*,QtDesigner*,QtExampleIcons*,QtGraphs*,QtGraphsWidgets*,QtHelp*,QtHttpServer*,QtLocation*,QtMultimedia*,QtMultimediaWidgets*,QtNetwork*,QtNetworkAuth*,QtNfc*,QtOpenGL*,QtOpenGLWidgets*,QtPdf*,QtPdfWidgets*,QtPositioning*,QtPrintSupport*,QtQml*,QtQuick*,QtQuick3D*,QtQuickControls2*,QtQuickTest*,QtQuickWidgets*,QtRemoteObjects*,QtScxml*,QtSensors*,QtSerialBus*,QtSerialPort*,QtSpatialAudio*,QtSql*,QtStateMachine*,QtSvgWidgets*,QtTest*,QtTextToSpeech*,QtUiTools*,QtWebChannel*,QtWebEngineCore*,QtWebEngineQuick*,QtWebEngineWidgets*,QtWebSockets*,QtWebView*,QtXml*}
|
||||
- shopt -s extglob
|
||||
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!(libQt6Core*|libQt6Gui*|libQt6Widgets*|libQt6OpenGL*|libQt6XcbQpa*|libQt6Wayland*|libQt6Egl*|libicudata*|libicuuc*|libicui18n*|libQt6DBus*|libQt6Svg*|libQt6Qml*|libQt6Network*)
|
||||
|
||||
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!(libQt6Core*|libQt6DBus*|libQt6Egl*|libQt6Gui*|libQt6Svg*|libQt6Wayland*|libQt6Widgets*|libQt6XcbQpa*|libicudata*|libicui18n*|libicuuc*)
|
||||
AppDir:
|
||||
path: ./AppDir
|
||||
|
||||
after_bundle:
|
||||
# Документация, справка, примеры
|
||||
- rm -rf $TARGET_APPDIR/usr/share/man || true
|
||||
- rm -rf $TARGET_APPDIR/usr/share/doc || true
|
||||
- rm -rf $TARGET_APPDIR/usr/share/doc-base || true
|
||||
- rm -rf $TARGET_APPDIR/usr/share/info || true
|
||||
- rm -rf $TARGET_APPDIR/usr/share/help || true
|
||||
- rm -rf $TARGET_APPDIR/usr/share/gtk-doc || true
|
||||
- rm -rf $TARGET_APPDIR/usr/share/devhelp || true
|
||||
- rm -rf $TARGET_APPDIR/usr/share/examples || true
|
||||
- rm -rf $TARGET_APPDIR/usr/share/pkgconfig || true
|
||||
- rm -rf $TARGET_APPDIR/usr/share/bash-completion || true
|
||||
- rm -rf $TARGET_APPDIR/usr/share/pixmaps || true
|
||||
- rm -rf $TARGET_APPDIR/usr/share/mime || true
|
||||
- rm -rf $TARGET_APPDIR/usr/share/metainfo || true
|
||||
- rm -rf $TARGET_APPDIR/usr/include || true
|
||||
- rm -rf $TARGET_APPDIR/usr/lib/pkgconfig || true
|
||||
# Статика и отладка
|
||||
- find $TARGET_APPDIR -type f \( -name '*.a' -o -name '*.la' -o -name '*.h' -o -name '*.cmake' -o -name '*.pdb' \) -delete || true
|
||||
# Strip ELF бинарников (исключая Python extensions)
|
||||
- "find $TARGET_APPDIR -type f -executable -exec file {} \\; | grep ELF | grep -v '/dist-packages/' | grep -v '/site-packages/' | cut -d: -f1 | xargs strip --strip-unneeded || true"
|
||||
# Удаление пустых папок
|
||||
- find $TARGET_APPDIR -type d -empty -delete || true
|
||||
app_info:
|
||||
id: ru.linux_gaming.PortProtonQt
|
||||
name: PortProtonQt
|
||||
icon: ru.linux_gaming.PortProtonQt
|
||||
version: 0.1.3
|
||||
version: 0.1.4
|
||||
exec: usr/bin/python3
|
||||
exec_args: "-m portprotonqt.app $@"
|
||||
|
||||
apt:
|
||||
arch: amd64
|
||||
sources:
|
||||
- sourceline: 'deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy main restricted universe multiverse'
|
||||
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x871920d1991bc93c'
|
||||
|
||||
include:
|
||||
- python3
|
||||
- python3-minimal
|
||||
- python3-pkg-resources
|
||||
- libopengl0
|
||||
- libk5crypto3
|
||||
@@ -45,13 +63,23 @@ AppDir:
|
||||
- libxcb-cursor0
|
||||
- libimage-exiftool-perl
|
||||
- xdg-utils
|
||||
exclude: []
|
||||
|
||||
exclude:
|
||||
# Документация и man-страницы
|
||||
- "*-doc"
|
||||
- "*-man"
|
||||
- manpages
|
||||
- mandb
|
||||
# Статические библиотеки
|
||||
- "*-dev"
|
||||
- "*-static"
|
||||
# Дебаг-символы
|
||||
- "*-dbg"
|
||||
- "*-dbgsym"
|
||||
runtime:
|
||||
env:
|
||||
PYTHONHOME: '${APPDIR}/usr'
|
||||
PYTHONPATH: '${APPDIR}/usr/local/lib/python3.10/dist-packages'
|
||||
|
||||
PERLLIB: '${APPDIR}/usr/share/perl5:${APPDIR}/usr/lib/x86_64-linux-gnu/perl/5.34:${APPDIR}/usr/share/perl/5.34'
|
||||
AppImage:
|
||||
sign-key: None
|
||||
arch: x86_64
|
||||
|
@@ -1,12 +1,12 @@
|
||||
pkgname=portprotonqt
|
||||
pkgver=0.1.3
|
||||
pkgver=0.1.4
|
||||
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-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client')
|
||||
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-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client')
|
||||
makedepends=('python-'{'build','installer','setuptools','wheel'})
|
||||
source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt.git")
|
||||
sha256sums=('SKIP')
|
||||
|
@@ -33,6 +33,7 @@ Requires: python3-babel
|
||||
Requires: python3-evdev
|
||||
Requires: python3-icoextract
|
||||
Requires: python3-numpy
|
||||
Requires: python3-websocket-client
|
||||
Requires: python3-orjson
|
||||
Requires: python3-psutil
|
||||
Requires: python3-pyside6
|
||||
@@ -44,6 +45,7 @@ Requires: python3-pefile
|
||||
Requires: python3-pillow
|
||||
Requires: perl-Image-ExifTool
|
||||
Requires: xdg-utils
|
||||
Requires: python3-beautifulsoup4
|
||||
|
||||
%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.3
|
||||
%global pypi_version 0.1.4
|
||||
%global oname PortProtonQt
|
||||
%global _python_no_extras_requires 1
|
||||
|
||||
@@ -30,6 +30,7 @@ Requires: python3-babel
|
||||
Requires: python3-evdev
|
||||
Requires: python3-icoextract
|
||||
Requires: python3-numpy
|
||||
Requires: python3-websocket-client
|
||||
Requires: python3-orjson
|
||||
Requires: python3-psutil
|
||||
Requires: python3-pyside6
|
||||
@@ -41,6 +42,7 @@ Requires: python3-pefile
|
||||
Requires: python3-pillow
|
||||
Requires: perl-Image-ExifTool
|
||||
Requires: xdg-utils
|
||||
Requires: python3-beautifulsoup4
|
||||
|
||||
%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.
|
||||
|
@@ -9,7 +9,7 @@ _portprotonqt() {
|
||||
esac
|
||||
|
||||
if [[ "$cur" == -* ]]; then
|
||||
COMPREPLY=( $( compgen -W '--fullscreen --session' -- "$cur" ) )
|
||||
COMPREPLY=( $( compgen -W '--fullscreen' -- "$cur" ) )
|
||||
return 0
|
||||
fi
|
||||
|
||||
|
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
"endpoint": "https://git.linux-gaming.ru/api/v1",
|
||||
"gitAuthor": "Renovate Bot <noreply@linux-gaming.ru>",
|
||||
"platform": "gitea",
|
||||
"onboardingConfigFileName": "renovate.json",
|
||||
"autodiscover": true,
|
||||
"optimizeForDisabled": true,
|
||||
};
|
@@ -765,7 +765,7 @@
|
||||
},
|
||||
{
|
||||
"normalized_name": "lost ark",
|
||||
"status": "Broken"
|
||||
"status": "Running"
|
||||
},
|
||||
{
|
||||
"normalized_name": "archeage unchained",
|
||||
@@ -4426,5 +4426,61 @@
|
||||
{
|
||||
"normalized_name": "carx street",
|
||||
"status": "Broken"
|
||||
},
|
||||
{
|
||||
"normalized_name": "warcos 2",
|
||||
"status": "Broken"
|
||||
},
|
||||
{
|
||||
"normalized_name": "karos classic",
|
||||
"status": "Broken"
|
||||
},
|
||||
{
|
||||
"normalized_name": "dead island riptide",
|
||||
"status": "Running"
|
||||
},
|
||||
{
|
||||
"normalized_name": "lineage",
|
||||
"status": "Broken"
|
||||
},
|
||||
{
|
||||
"normalized_name": "day of dragons",
|
||||
"status": "Running"
|
||||
},
|
||||
{
|
||||
"normalized_name": "sonic rumble",
|
||||
"status": "Broken"
|
||||
},
|
||||
{
|
||||
"normalized_name": "black stigma",
|
||||
"status": "Broken"
|
||||
},
|
||||
{
|
||||
"normalized_name": "umamusume pretty derby",
|
||||
"status": "Running"
|
||||
},
|
||||
{
|
||||
"normalized_name": "dirt rally",
|
||||
"status": "Supported"
|
||||
},
|
||||
{
|
||||
"normalized_name": "minifighter",
|
||||
"status": "Broken"
|
||||
},
|
||||
{
|
||||
"normalized_name": "hide & hold out h2o",
|
||||
"status": "Running"
|
||||
},
|
||||
{
|
||||
"normalized_name": "f1 25",
|
||||
"status": "Denied"
|
||||
},
|
||||
{
|
||||
"normalized_name": "ghost of tsushima director's cut",
|
||||
"status": "Denied"
|
||||
},
|
||||
{
|
||||
"normalized_name": "sword of justice",
|
||||
"status": "Broken"
|
||||
}
|
||||
]
|
@@ -1,12 +1,56 @@
|
||||
[
|
||||
{
|
||||
"normalized_title": "return alive",
|
||||
"slug": "return-alive"
|
||||
"normalized_title": "no sleep for kaname date from ai the somnium files",
|
||||
"slug": "no-sleep-for-kaname-date-from-ai-the-somnium-files"
|
||||
},
|
||||
{
|
||||
"normalized_title": "dead island 2",
|
||||
"slug": "dead-island-2"
|
||||
},
|
||||
{
|
||||
"normalized_title": "dead island",
|
||||
"slug": "dead-island-definitive-edition"
|
||||
},
|
||||
{
|
||||
"normalized_title": "wuchang fallen feathers",
|
||||
"slug": "wuchang-fallen-feathers"
|
||||
},
|
||||
{
|
||||
"normalized_title": "mindseye",
|
||||
"slug": "mindseye"
|
||||
},
|
||||
{
|
||||
"normalized_title": "alan wake",
|
||||
"slug": "alan-wake"
|
||||
},
|
||||
{
|
||||
"normalized_title": "s.t.a.l.k.e.r. anomaly g.a.m.m.a",
|
||||
"slug": "s-t-a-l-k-e-r-anomaly-g-a-m-m-a"
|
||||
},
|
||||
{
|
||||
"normalized_title": "fifa 18",
|
||||
"slug": "fifa-18"
|
||||
},
|
||||
{
|
||||
"normalized_title": "eriksholm the stolen dream",
|
||||
"slug": "eriksholm-the-stolen-dream"
|
||||
},
|
||||
{
|
||||
"normalized_title": "caravan sandwitch",
|
||||
"slug": "caravan-sandwitch"
|
||||
},
|
||||
{
|
||||
"normalized_title": "expeditions a mudrunner game",
|
||||
"slug": "expeditions-a-mudrunner-game"
|
||||
},
|
||||
{
|
||||
"normalized_title": "#drive rally",
|
||||
"slug": "drive-rally"
|
||||
},
|
||||
{
|
||||
"normalized_title": "return alive",
|
||||
"slug": "return-alive"
|
||||
},
|
||||
{
|
||||
"normalized_title": "recore",
|
||||
"slug": "recore-definitive-edition"
|
||||
|
378
dev-scripts/appimage_clean.py
Executable file
@@ -0,0 +1,378 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PySide6 Dependencies Analyzer with ldd support
|
||||
Анализирует зависимости PySide6 модулей используя ldd для определения
|
||||
реальных зависимостей скомпилированных библиотек.
|
||||
"""
|
||||
|
||||
import ast
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Set, Dict, List
|
||||
import argparse
|
||||
import json
|
||||
|
||||
|
||||
class PySide6DependencyAnalyzer:
|
||||
def __init__(self):
|
||||
# Системные библиотеки, которые нужно всегда оставлять
|
||||
self.system_libs = {
|
||||
'libQt6XcbQpa', 'libQt6Wayland', 'libQt6Egl',
|
||||
'libicudata', 'libicuuc', 'libicui18n', 'libQt6DBus'
|
||||
}
|
||||
|
||||
self.real_dependencies = {}
|
||||
self.used_modules_code = set()
|
||||
self.used_modules_ldd = set()
|
||||
self.all_required_modules = set()
|
||||
|
||||
def find_python_files(self, directory: Path) -> List[Path]:
|
||||
"""Находит все Python файлы в директории"""
|
||||
python_files = []
|
||||
for root, dirs, files in os.walk(directory):
|
||||
dirs[:] = [d for d in dirs if d not in {'.venv', '__pycache__', '.git'}]
|
||||
|
||||
for file in files:
|
||||
if file.endswith('.py'):
|
||||
python_files.append(Path(root) / file)
|
||||
return python_files
|
||||
|
||||
def find_pyside6_libs(self, base_path: Path) -> Dict[str, Path]:
|
||||
"""Находит все PySide6 библиотеки (.so файлы)"""
|
||||
libs = {}
|
||||
|
||||
# Поиск в единственной локации
|
||||
search_path = Path("../.venv/lib/python3.10/site-packages/PySide6")
|
||||
print(f"Поиск PySide6 библиотек в: {search_path}")
|
||||
|
||||
if search_path.exists():
|
||||
# Ищем .so файлы модулей
|
||||
for so_file in search_path.glob("Qt*.*.so"):
|
||||
module_name = so_file.stem.split('.')[0] # QtCore.abi3.so -> QtCore
|
||||
if module_name.startswith('Qt'):
|
||||
libs[module_name] = so_file
|
||||
|
||||
# Также ищем в подпапках
|
||||
for subdir in search_path.iterdir():
|
||||
if subdir.is_dir() and subdir.name.startswith('Qt'):
|
||||
for so_file in subdir.glob("*.so*"):
|
||||
if 'Qt' in so_file.name:
|
||||
libs[subdir.name] = so_file
|
||||
break
|
||||
|
||||
return libs
|
||||
|
||||
def analyze_ldd_dependencies(self, lib_path: Path) -> Set[str]:
|
||||
"""Анализирует зависимости библиотеки с помощью ldd"""
|
||||
qt_deps = set()
|
||||
|
||||
try:
|
||||
result = subprocess.run(['ldd', str(lib_path)],
|
||||
capture_output=True, text=True, check=True)
|
||||
|
||||
# Парсим вывод ldd и ищем Qt библиотеки
|
||||
for line in result.stdout.split('\n'):
|
||||
# Ищем строки вида: libQt6Core.so.6 => /path/to/lib
|
||||
match = re.search(r'libQt6(\w+)\.so', line)
|
||||
if match:
|
||||
qt_module = f"Qt{match.group(1)}"
|
||||
qt_deps.add(qt_module)
|
||||
|
||||
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
||||
print(f"Предупреждение: не удалось выполнить ldd для {lib_path}: {e}")
|
||||
|
||||
return qt_deps
|
||||
|
||||
def build_real_dependency_graph(self, pyside_libs: Dict[str, Path]) -> Dict[str, Set[str]]:
|
||||
"""Строит граф зависимостей на основе ldd анализа"""
|
||||
dependencies = {}
|
||||
|
||||
print("Анализ реальных зависимостей с помощью ldd...")
|
||||
for module, lib_path in pyside_libs.items():
|
||||
print(f" Анализируется {module}...")
|
||||
deps = self.analyze_ldd_dependencies(lib_path)
|
||||
dependencies[module] = deps
|
||||
|
||||
if deps:
|
||||
print(f" Зависимости: {', '.join(sorted(deps))}")
|
||||
|
||||
return dependencies
|
||||
|
||||
def analyze_file_imports(self, file_path: Path) -> Set[str]:
|
||||
"""Анализирует один Python файл и возвращает используемые PySide6 модули"""
|
||||
modules = set()
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
content = f.read()
|
||||
|
||||
tree = ast.parse(content)
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
if alias.name.startswith('PySide6.'):
|
||||
module = alias.name.split('.', 2)[1]
|
||||
if module.startswith('Qt'):
|
||||
modules.add(module)
|
||||
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
if node.module and node.module.startswith('PySide6.'):
|
||||
module = node.module.split('.', 2)[1]
|
||||
if module.startswith('Qt'):
|
||||
modules.add(module)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка при анализе {file_path}: {e}")
|
||||
|
||||
return modules
|
||||
|
||||
def get_all_dependencies(self, modules: Set[str], dependency_graph: Dict[str, Set[str]]) -> Set[str]:
|
||||
"""Получает все зависимости для набора модулей, используя граф зависимостей из ldd"""
|
||||
all_deps = set(modules)
|
||||
|
||||
if not dependency_graph:
|
||||
return all_deps
|
||||
|
||||
# Повторяем до тех пор, пока не найдем все транзитивные зависимости
|
||||
changed = True
|
||||
iteration = 0
|
||||
while changed and iteration < 10: # Защита от бесконечного цикла
|
||||
changed = False
|
||||
current_deps = set(all_deps)
|
||||
|
||||
for module in current_deps:
|
||||
if module in dependency_graph:
|
||||
new_deps = dependency_graph[module] - all_deps
|
||||
if new_deps:
|
||||
all_deps.update(new_deps)
|
||||
changed = True
|
||||
|
||||
iteration += 1
|
||||
|
||||
return all_deps
|
||||
|
||||
def analyze_project(self, project_path: Path, appdir_path: Path = None) -> Dict:
|
||||
"""Анализирует весь проект"""
|
||||
python_files = self.find_python_files(project_path)
|
||||
print(f"Найдено {len(python_files)} Python файлов")
|
||||
|
||||
# Анализ статических импортов
|
||||
used_modules_code = set()
|
||||
file_modules = {}
|
||||
|
||||
for file_path in python_files:
|
||||
modules = self.analyze_file_imports(file_path)
|
||||
if modules:
|
||||
file_modules[str(file_path.relative_to(project_path))] = list(modules)
|
||||
used_modules_code.update(modules)
|
||||
|
||||
print(f"Найдено {len(used_modules_code)} модулей в коде: {', '.join(sorted(used_modules_code))}")
|
||||
|
||||
# Поиск PySide6 библиотек
|
||||
search_base = appdir_path if appdir_path else project_path
|
||||
pyside_libs = self.find_pyside6_libs(search_base)
|
||||
|
||||
if not pyside_libs:
|
||||
print("ОШИБКА: PySide6 библиотеки не найдены! Анализ невозможен.")
|
||||
return {
|
||||
'error': 'PySide6 библиотеки не найдены',
|
||||
'analysis_method': 'failed',
|
||||
'found_libraries': 0,
|
||||
'directly_used_code': sorted(used_modules_code),
|
||||
'all_required': [],
|
||||
'removable': [],
|
||||
'available_modules': [],
|
||||
'file_usage': file_modules
|
||||
}
|
||||
|
||||
print(f"Найдено {len(pyside_libs)} PySide6 библиотек")
|
||||
|
||||
# Анализ реальных зависимостей с ldd
|
||||
real_dependencies = self.build_real_dependency_graph(pyside_libs)
|
||||
|
||||
# Определяем модули, которые реально используются через ldd
|
||||
used_modules_ldd = set()
|
||||
for module in used_modules_code:
|
||||
if module in real_dependencies:
|
||||
used_modules_ldd.update(real_dependencies[module])
|
||||
used_modules_ldd.add(module)
|
||||
|
||||
print(f"Реальные зависимости через ldd: {', '.join(sorted(used_modules_ldd))}")
|
||||
|
||||
# Объединяем результаты анализа кода и ldd
|
||||
all_used_modules = used_modules_code | used_modules_ldd
|
||||
|
||||
# Получаем все необходимые модули включая зависимости
|
||||
all_required = self.get_all_dependencies(all_used_modules, real_dependencies)
|
||||
|
||||
# Все доступные PySide6 модули
|
||||
available_modules = set(pyside_libs.keys())
|
||||
|
||||
# Модули, которые можно удалить
|
||||
removable = available_modules - all_required
|
||||
|
||||
return {
|
||||
'analysis_method': 'ldd + static analysis',
|
||||
'found_libraries': len(pyside_libs),
|
||||
'directly_used_code': sorted(used_modules_code),
|
||||
'directly_used_ldd': sorted(used_modules_ldd),
|
||||
'all_required': sorted(all_required),
|
||||
'removable': sorted(removable),
|
||||
'available_modules': sorted(available_modules),
|
||||
'file_usage': file_modules,
|
||||
'real_dependencies': {k: sorted(v) for k, v in real_dependencies.items()},
|
||||
'library_paths': {k: str(v) for k, v in pyside_libs.items()},
|
||||
'analysis_summary': {
|
||||
'total_modules': len(available_modules),
|
||||
'required_modules': len(all_required),
|
||||
'removable_modules': len(removable),
|
||||
'space_saving_potential': f"{len(removable)/len(available_modules)*100:.1f}%" if available_modules else "0%"
|
||||
}
|
||||
}
|
||||
|
||||
def generate_appimage_recipe(self, removable_modules: List[str], template_path: Path) -> str:
|
||||
"""Генерирует обновленный AppImage рецепт с командами очистки"""
|
||||
|
||||
# Читаем существующий рецепт
|
||||
try:
|
||||
with open(template_path, 'r', encoding='utf-8') as f:
|
||||
recipe_content = f.read()
|
||||
except FileNotFoundError:
|
||||
print(f"Шаблон рецепта не найден: {template_path}")
|
||||
return ""
|
||||
|
||||
# Генерируем новые команды очистки
|
||||
cleanup_lines = []
|
||||
|
||||
# QML удаляем только если не используется
|
||||
qml_modules = {'QtQml', 'QtQuick', 'QtQuickWidgets'}
|
||||
if qml_modules.issubset(set(removable_modules)):
|
||||
cleanup_lines.append(" - rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/qml/")
|
||||
|
||||
# Инструменты разработки (всегда удаляем)
|
||||
cleanup_lines.append(" - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{assistant,designer,linguist,lrelease,lupdate}")
|
||||
|
||||
# Модули для удаления
|
||||
if removable_modules:
|
||||
modules_list = ','.join([f"{mod}*" for mod in sorted(removable_modules)])
|
||||
cleanup_lines.append(f" - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{{{modules_list}}}")
|
||||
|
||||
# Генерируем команду для удаления нативных библиотек с сохранением нужных
|
||||
required_libs = set()
|
||||
for module in sorted(set(self.all_required_modules)):
|
||||
required_libs.add(f"libQt6{module.replace('Qt', '')}*")
|
||||
|
||||
# Добавляем системные библиотеки
|
||||
for lib in self.system_libs:
|
||||
required_libs.add(f"{lib}*")
|
||||
|
||||
keep_pattern = '|'.join(sorted(required_libs))
|
||||
|
||||
cleanup_lines.extend([
|
||||
" - shopt -s extglob",
|
||||
f" - rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!({keep_pattern})"
|
||||
])
|
||||
|
||||
# Заменяем блок очистки в рецепте
|
||||
import re
|
||||
|
||||
# Ищем блок "# 5) чистим от ненужных модулей и бинарников" до следующего комментария или до AppDir:
|
||||
pattern = r'( # 5\) чистим от ненужных модулей и бинарников\n).*?(?=\nAppDir:|\n # [0-9]+\)|$)'
|
||||
|
||||
new_cleanup_block = " # 5) чистим от ненужных модулей и бинарников\n" + '\n'.join(cleanup_lines)
|
||||
|
||||
updated_recipe = re.sub(pattern, new_cleanup_block, recipe_content, flags=re.DOTALL)
|
||||
|
||||
return updated_recipe
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Анализ зависимостей PySide6 модулей с использованием ldd')
|
||||
parser.add_argument('project_path', help='Путь к проекту для анализа')
|
||||
parser.add_argument('--appdir', help='Путь к AppDir для поиска PySide6 библиотек')
|
||||
parser.add_argument('--output', '-o', help='Путь для сохранения результатов (JSON)')
|
||||
parser.add_argument('--verbose', '-v', action='store_true', help='Подробный вывод')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
project_path = Path(args.project_path)
|
||||
if not project_path.exists():
|
||||
print(f"Ошибка: путь {project_path} не существует")
|
||||
sys.exit(1)
|
||||
|
||||
appdir_path = Path(args.appdir) if args.appdir else None
|
||||
if appdir_path and not appdir_path.exists():
|
||||
print(f"Предупреждение: AppDir путь {appdir_path} не существует")
|
||||
appdir_path = None
|
||||
|
||||
analyzer = PySide6DependencyAnalyzer()
|
||||
results = analyzer.analyze_project(project_path, appdir_path)
|
||||
|
||||
# Сохраняем в анализатор для генерации команд
|
||||
analyzer.all_required_modules = set(results.get('all_required', []))
|
||||
|
||||
# Выводим результаты
|
||||
print("\n" + "="*60)
|
||||
print("АНАЛИЗ ЗАВИСИМОСТЕЙ PYSIDE6 (ldd analysis)")
|
||||
print("="*60)
|
||||
|
||||
if 'error' in results:
|
||||
print(f"\nОШИБКА: {results['error']}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"\nМетод анализа: {results['analysis_method']}")
|
||||
print(f"Найдено библиотек: {results['found_libraries']}")
|
||||
|
||||
if results['directly_used_code']:
|
||||
print(f"\nИспользуемые модули в коде ({len(results['directly_used_code'])}):")
|
||||
for module in results['directly_used_code']:
|
||||
print(f" • {module}")
|
||||
|
||||
if results['directly_used_ldd']:
|
||||
print(f"\nРеальные зависимости через ldd ({len(results['directly_used_ldd'])}):")
|
||||
for module in results['directly_used_ldd']:
|
||||
print(f" • {module}")
|
||||
|
||||
print(f"\nВсе необходимые модули ({len(results['all_required'])}):")
|
||||
for module in results['all_required']:
|
||||
print(f" • {module}")
|
||||
|
||||
print(f"\nМодули, которые можно удалить ({len(results['removable'])}):")
|
||||
for module in results['removable']:
|
||||
print(f" • {module}")
|
||||
|
||||
print(f"\nПотенциальная экономия места: {results['analysis_summary']['space_saving_potential']}")
|
||||
|
||||
if args.verbose and results['real_dependencies']:
|
||||
Devlin(f"\nРеальные зависимости (ldd):")
|
||||
for module, deps in results['real_dependencies'].items():
|
||||
if deps:
|
||||
print(f" {module} → {', '.join(deps)}")
|
||||
|
||||
# Обновляем AppImage рецепт
|
||||
recipe_path = Path("../build-aux/AppImageBuilder.yml")
|
||||
if recipe_path.exists():
|
||||
updated_recipe = analyzer.generate_appimage_recipe(results['removable'], recipe_path)
|
||||
if updated_recipe:
|
||||
with open(recipe_path, 'w', encoding='utf-8') as f:
|
||||
f.write(updated_recipe)
|
||||
print(f"\nAppImage рецепт обновлен: {recipe_path}")
|
||||
else:
|
||||
print(f"\nОШИБКА: не удалось обновить рецепт")
|
||||
else:
|
||||
print(f"\nПредупреждение: рецепт AppImage не найден в {recipe_path}")
|
||||
|
||||
# Сохраняем результаты в JSON
|
||||
if args.output:
|
||||
with open(args.output, 'w', encoding='utf-8') as f:
|
||||
json.dump(results, f, ensure_ascii=False, indent=2)
|
||||
print(f"Результаты сохранены в: {args.output}")
|
||||
|
||||
print("\n" + "="*60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@@ -3,10 +3,11 @@
|
||||
---
|
||||
|
||||
## 📋 Contents
|
||||
- [Overview](#overview)
|
||||
- [Adding a New Translation](#adding-a-new-translation)
|
||||
- [Updating Existing Translations](#updating-existing-translations)
|
||||
- [Compiling Translations](#compiling-translations)
|
||||
- [Overview](#-overview)
|
||||
- [Adding a New Translation](#-adding-a-new-translation)
|
||||
- [Updating Existing Translations](#-updating-existing-translations)
|
||||
- [Compiling Translations](#-compiling-translations)
|
||||
- [Spell Check](#-spell-check)
|
||||
|
||||
---
|
||||
|
||||
@@ -20,9 +21,9 @@ Current translation status:
|
||||
|
||||
| Locale | Progress | Translated |
|
||||
| :----- | -------: | ---------: |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 194 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 194 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 194 of 194 |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 197 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 197 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 197 of 197 |
|
||||
|
||||
---
|
||||
|
||||
|
@@ -3,10 +3,11 @@
|
||||
---
|
||||
|
||||
## 📋 Содержание
|
||||
- [Обзор](#обзор)
|
||||
- [Добавление нового перевода](#добавление-нового-перевода)
|
||||
- [Обновление существующих переводов](#обновление-существующих-переводов)
|
||||
- [Компиляция переводов](#компиляция-переводов)
|
||||
- [Обзор](#-обзор)
|
||||
- [Добавление нового перевода](#-добавление-нового-перевода)
|
||||
- [Обновление существующих переводов](#-обновление-существующих-переводов)
|
||||
- [Компиляция переводов](#-компиляция-переводов)
|
||||
- [Проверка орфографии](#-проверка-орфографии)
|
||||
|
||||
---
|
||||
|
||||
@@ -20,9 +21,9 @@
|
||||
|
||||
| Локаль | Прогресс | Переведено |
|
||||
| :----- | -------: | ---------: |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 194 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 194 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 194 из 194 |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 197 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 197 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 197 из 197 |
|
||||
|
||||
---
|
||||
|
||||
|
@@ -3,15 +3,10 @@
|
||||
---
|
||||
|
||||
## 📋 Contents
|
||||
- [Overview](#overview)
|
||||
- [How It Works](#how-it-works)
|
||||
- [Data Priorities](#data-priorities)
|
||||
- [File Structure](#file-structure)
|
||||
- [For Users](#for-users)
|
||||
- [Creating User Overrides](#creating-user-overrides)
|
||||
- [Example](#example)
|
||||
- [For Developers](#for-developers)
|
||||
- [Adding Built-In Overrides](#adding-built-in-overrides)
|
||||
- [Overview](#-overview)
|
||||
- [How It Works](#-how-it-works)
|
||||
- [For Users](#-for-users)
|
||||
- [For Developers](#-for-developers)
|
||||
|
||||
---
|
||||
|
||||
|
@@ -3,15 +3,10 @@
|
||||
---
|
||||
|
||||
## 📋 Содержание
|
||||
- [Обзор](#обзор)
|
||||
- [Как это работает](#как-это-работает)
|
||||
- [Приоритеты данных](#приоритеты-данных)
|
||||
- [Структура файлов](#структура-файлов)
|
||||
- [Для пользователей](#для-пользователей)
|
||||
- [Создание пользовательских переопределений](#создание-пользовательских-переопределений)
|
||||
- [Пример](#пример)
|
||||
- [Для разработчиков](#для-разработчиков)
|
||||
- [Добавление встроенных переопределений](#добавление-встроенных-переопределений)
|
||||
- [Обзор](#-обзор)
|
||||
- [Как это работает](#-как-это-работает)
|
||||
- [Для пользователей](#-для-пользователей)
|
||||
- [Для разработчиков](#-для-разработчиков)
|
||||
|
||||
---
|
||||
|
||||
|
@@ -3,12 +3,13 @@
|
||||
---
|
||||
|
||||
## 📋 Contents
|
||||
- [Overview](#overview)
|
||||
- [Creating the Theme Folder](#creating-the-theme-folder)
|
||||
- [Style File](#style-file)
|
||||
- [Metadata](#metadata)
|
||||
- [Screenshots](#screenshots)
|
||||
- [Fonts and Icons](#fonts-and-icons)
|
||||
- [Overview](#-overview)
|
||||
- [Creating the Theme Folder](#-creating-the-theme-folder)
|
||||
- [Style File](#-style-file-stylespy)
|
||||
- [Animation configuration](#-animation-configuration)
|
||||
- [Metadata](#-metadata-metainfoini)
|
||||
- [Screenshots](#-screenshots)
|
||||
- [Fonts and Icons](#-fonts-and-icons-optional)
|
||||
|
||||
---
|
||||
|
||||
@@ -45,6 +46,114 @@ def custom_button_style(color1, color2):
|
||||
|
||||
---
|
||||
|
||||
## 🎥 Animation configuration
|
||||
|
||||
The `GAME_CARD_ANIMATION` dictionary controls all animation parameters for game cards:
|
||||
|
||||
```python
|
||||
GAME_CARD_ANIMATION = {
|
||||
# Type of animation when entering and exiting the detail page
|
||||
# Possible values: "fade", "slide_left", "slide_right", "slide_up", "slide_down", "bounce"
|
||||
"detail_page_animation_type": "fade",
|
||||
|
||||
# Border width of the card in idle state (no hover or focus).
|
||||
# Affects the thickness of the border when the card is not highlighted.
|
||||
# Value in pixels.
|
||||
"default_border_width": 2,
|
||||
|
||||
# Border width on hover.
|
||||
# Increases the border thickness when the cursor is over the card.
|
||||
# Value in pixels.
|
||||
"hover_border_width": 8,
|
||||
|
||||
# Border width on focus (e.g., selected via keyboard).
|
||||
# Increases the border thickness when the card is focused.
|
||||
# Value in pixels.
|
||||
"focus_border_width": 12,
|
||||
|
||||
# Minimum border width during pulsing animation.
|
||||
# Sets the minimum border thickness during the "breathing" animation.
|
||||
# Value in pixels.
|
||||
"pulse_min_border_width": 8,
|
||||
|
||||
# Maximum border width during pulsing animation.
|
||||
# Sets the maximum border thickness during pulsing.
|
||||
# Value in pixels.
|
||||
"pulse_max_border_width": 10,
|
||||
|
||||
# Duration of the border thickness animation (e.g., on hover or focus).
|
||||
# Affects the speed of transition between different border widths.
|
||||
# Value in milliseconds.
|
||||
"thickness_anim_duration": 300,
|
||||
|
||||
# Duration of one pulsing animation cycle.
|
||||
# Defines how fast the border "pulses" between min and max values.
|
||||
# Value in milliseconds.
|
||||
"pulse_anim_duration": 800,
|
||||
|
||||
# Duration of the gradient rotation animation.
|
||||
# Affects how fast the gradient border rotates around the card.
|
||||
# Value in milliseconds.
|
||||
"gradient_anim_duration": 3000,
|
||||
|
||||
# Starting angle of the gradient (in degrees).
|
||||
# Defines the initial rotation point of the gradient when the animation starts.
|
||||
"gradient_start_angle": 360,
|
||||
|
||||
# Ending angle of the gradient (in degrees).
|
||||
# Defines the end rotation point of the gradient.
|
||||
# A value of 0 means a full 360-degree rotation.
|
||||
"gradient_end_angle": 0,
|
||||
|
||||
# Easing curve type for border expansion animation (on hover/focus).
|
||||
# Affects the "feel" of the animation (e.g., smooth acceleration or deceleration).
|
||||
# Possible values: strings corresponding to QEasingCurve.Type (e.g., "OutBack", "InOutQuad").
|
||||
"thickness_easing_curve": "OutBack",
|
||||
|
||||
# Easing curve type for border contraction animation (on mouse leave/focus loss).
|
||||
# Affects the "feel" of returning to the original border width.
|
||||
"thickness_easing_curve_out": "InBack",
|
||||
|
||||
# Gradient colors for the animated border.
|
||||
# A list of dictionaries where each defines a position (0.0–1.0) and color in hex format.
|
||||
# Affects the appearance of the border on hover or focus.
|
||||
"gradient_colors": [
|
||||
{"position": 0, "color": "#00fff5"}, # Start color (cyan)
|
||||
{"position": 0.33, "color": "#FF5733"}, # 33% color (orange)
|
||||
{"position": 0.66, "color": "#9B59B6"}, # 66% color (purple)
|
||||
{"position": 1, "color": "#00fff5"} # End color (back to cyan)
|
||||
],
|
||||
|
||||
# Duration of the fade animation when entering the detail page
|
||||
"detail_page_fade_duration": 350,
|
||||
|
||||
# Duration of the slide animation when entering the detail page
|
||||
"detail_page_slide_duration": 500,
|
||||
|
||||
# Duration of the bounce animation when entering the detail page
|
||||
"detail_page_bounce_duration": 400,
|
||||
|
||||
# Duration of the fade animation when exiting the detail page
|
||||
"detail_page_fade_duration_exit": 350,
|
||||
|
||||
# Duration of the slide animation when exiting the detail page
|
||||
"detail_page_slide_duration_exit": 500,
|
||||
|
||||
# Duration of the bounce animation when exiting the detail page
|
||||
"detail_page_bounce_duration_exit": 400,
|
||||
|
||||
# Easing curve type for animation when entering the detail page
|
||||
# Applies to slide and bounce animations
|
||||
"detail_page_easing_curve": "OutCubic",
|
||||
|
||||
# Easing curve type for animation when exiting the detail page
|
||||
# Applies to slide and bounce animations
|
||||
"detail_page_easing_curve_exit": "InCubic"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Metadata (`metainfo.ini`)
|
||||
|
||||
```ini
|
||||
|
@@ -3,12 +3,13 @@
|
||||
---
|
||||
|
||||
## 📋 Содержание
|
||||
- [Обзор](#обзор)
|
||||
- [Создание папки темы](#создание-папки-темы)
|
||||
- [Файл стилей](#файл-стилей)
|
||||
- [Метаинформация](#метаинформация)
|
||||
- [Скриншоты](#скриншоты)
|
||||
- [Шрифты и иконки](#шрифты-и-иконки)
|
||||
- [Обзор](#-обзор)
|
||||
- [Создание папки темы](#-создание-папки-темы)
|
||||
- [Файл стилей](#-файл-стилей-stylespy)
|
||||
- [Конфигурация анимации](#-конфигурация-анимации)
|
||||
- [Метаинформация](#-метаинформация-metainfoini)
|
||||
- [Скриншоты](#-скриншоты)
|
||||
- [Шрифты и иконки](#-шрифты-и-иконки-опционально)
|
||||
|
||||
---
|
||||
|
||||
@@ -45,6 +46,114 @@ def custom_button_style(color1, color2):
|
||||
|
||||
---
|
||||
|
||||
## 🎥 Конфигурация анимации
|
||||
|
||||
Словарь `GAME_CARD_ANIMATION` управляет всеми параметрами анимации для карточек игр:
|
||||
|
||||
```python
|
||||
GAME_CARD_ANIMATION = {
|
||||
# Тип анимации при входе и выходе на детальную страницу
|
||||
# Возможные значения: "fade", "slide_left", "slide_right", "slide_up", "slide_down", "bounce"
|
||||
"detail_page_animation_type": "fade",
|
||||
|
||||
# Ширина обводки карточки в состоянии покоя (без наведения или фокуса).
|
||||
# Влияет на толщину рамки вокруг карточки, когда она не выделена.
|
||||
# Значение в пикселях.
|
||||
"default_border_width": 2,
|
||||
|
||||
# Ширина обводки при наведении курсора.
|
||||
# Увеличивает толщину рамки, когда курсор находится над карточкой.
|
||||
# Значение в пикселях.
|
||||
"hover_border_width": 8,
|
||||
|
||||
# Ширина обводки при фокусе (например, при выборе с клавиатуры).
|
||||
# Увеличивает толщину рамки, когда карточка в фокусе.
|
||||
# Значение в пикселях.
|
||||
"focus_border_width": 12,
|
||||
|
||||
# Минимальная ширина обводки во время пульсирующей анимации.
|
||||
# Определяет минимальную толщину рамки при пульсации (анимация "дыхания").
|
||||
# Значение в пикселях.
|
||||
"pulse_min_border_width": 8,
|
||||
|
||||
# Максимальная ширина обводки во время пульсирующей анимации.
|
||||
# Определяет максимальную толщину рамки при пульсации.
|
||||
# Значение в пикселях.
|
||||
"pulse_max_border_width": 10,
|
||||
|
||||
# Длительность анимации изменения толщины обводки (например, при наведении или фокусе).
|
||||
# Влияет на скорость перехода от одной ширины обводки к другой.
|
||||
# Значение в миллисекундах.
|
||||
"thickness_anim_duration": 300,
|
||||
|
||||
# Длительность одного цикла пульсирующей анимации.
|
||||
# Определяет, как быстро рамка "пульсирует" между min и max значениями.
|
||||
# Значение в миллисекундах.
|
||||
"pulse_anim_duration": 800,
|
||||
|
||||
# Длительность анимации вращения градиента.
|
||||
# Влияет на скорость, с которой градиентная обводка вращается вокруг карточки.
|
||||
# Значение в миллисекундах.
|
||||
"gradient_anim_duration": 3000,
|
||||
|
||||
# Начальный угол градиента (в градусах).
|
||||
# Определяет начальную точку вращения градиента при старте анимации.
|
||||
"gradient_start_angle": 360,
|
||||
|
||||
# Конечный угол градиента (в градусах).
|
||||
# Определяет конечную точку вращения градиента.
|
||||
# Значение 0 означает полный поворот на 360 градусов.
|
||||
"gradient_end_angle": 0,
|
||||
|
||||
# Тип кривой сглаживания для анимации увеличения обводки (при наведении/фокусе).
|
||||
# Влияет на "чувство" анимации (например, плавное ускорение или замедление).
|
||||
# Возможные значения: строки, соответствующие QEasingCurve.Type (например, "OutBack", "InOutQuad").
|
||||
"thickness_easing_curve": "OutBack",
|
||||
|
||||
# Тип кривой сглаживания для анимации уменьшения обводки (при уходе курсора/потере фокуса).
|
||||
# Влияет на "чувство" возврата к исходной ширине обводки.
|
||||
"thickness_easing_curve_out": "InBack",
|
||||
|
||||
# Цвета градиента для анимированной обводки.
|
||||
# Список словарей, где каждый словарь задает позицию (0.0–1.0) и цвет в формате hex.
|
||||
# Влияет на внешний вид обводки при наведении или фокусе.
|
||||
"gradient_colors": [
|
||||
{"position": 0, "color": "#00fff5"}, # Начальный цвет (циан)
|
||||
{"position": 0.33, "color": "#FF5733"}, # Цвет на 33% (оранжевый)
|
||||
{"position": 0.66, "color": "#9B59B6"}, # Цвет на 66% (пурпурный)
|
||||
{"position": 1, "color": "#00fff5"} # Конечный цвет (возвращение к циану)
|
||||
],
|
||||
|
||||
# Длительность анимации fade при входе на детальную страницу
|
||||
"detail_page_fade_duration": 350,
|
||||
|
||||
# Длительность анимации slide при входе на детальную страницу
|
||||
"detail_page_slide_duration": 500,
|
||||
|
||||
# Длительность анимации bounce при входе на детальную страницу
|
||||
"detail_page_bounce_duration": 400,
|
||||
|
||||
# Длительность анимации fade при выходе из детальной страницы
|
||||
"detail_page_fade_duration_exit": 350,
|
||||
|
||||
# Длительность анимации slide при выходе из детальной страницы
|
||||
"detail_page_slide_duration_exit": 500,
|
||||
|
||||
# Длительность анимации bounce при выходе из детальной страницы
|
||||
"detail_page_bounce_duration_exit": 400,
|
||||
|
||||
# Тип кривой сглаживания для анимации при входе на детальную страницу
|
||||
# Применяется к slide и bounce анимациям
|
||||
"detail_page_easing_curve": "OutCubic",
|
||||
|
||||
# Тип кривой сглаживания для анимации при выходе из детальной страницы
|
||||
# Применяется к slide и bounce анимациям
|
||||
"detail_page_easing_curve_exit": "InCubic"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Метаинформация (`metainfo.ini`)
|
||||
|
||||
```ini
|
||||
|
328
portprotonqt/animations.py
Normal file
@@ -0,0 +1,328 @@
|
||||
from PySide6.QtCore import QPropertyAnimation, QByteArray, QEasingCurve, QAbstractAnimation, QParallelAnimationGroup, QRect, Qt, QPoint
|
||||
from PySide6.QtGui import QPainter, QPen, QColor, QConicalGradient, QBrush
|
||||
from PySide6.QtWidgets import QWidget, QGraphicsOpacityEffect
|
||||
from collections.abc import Callable
|
||||
import portprotonqt.themes.standart.styles as default_styles
|
||||
from portprotonqt.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
class SafeOpacityEffect(QGraphicsOpacityEffect):
|
||||
def __init__(self, parent=None, disable_at_full=True):
|
||||
super().__init__(parent)
|
||||
self.disable_at_full = disable_at_full
|
||||
|
||||
def setOpacity(self, opacity: float):
|
||||
opacity = max(0.0, min(1.0, opacity))
|
||||
super().setOpacity(opacity)
|
||||
if opacity < 1.0:
|
||||
self.setEnabled(True)
|
||||
elif self.disable_at_full:
|
||||
self.setEnabled(False)
|
||||
|
||||
class GameCardAnimations:
|
||||
def __init__(self, game_card, theme=None):
|
||||
self.game_card = game_card
|
||||
self.theme = theme if theme is not None else default_styles
|
||||
self.thickness_anim: QPropertyAnimation | None = None
|
||||
self.gradient_anim: QPropertyAnimation | None = None
|
||||
self.pulse_anim: QPropertyAnimation | None = None
|
||||
self._isPulseAnimationConnected = False
|
||||
|
||||
def setup_animations(self):
|
||||
"""Initialize animation properties."""
|
||||
self.thickness_anim = QPropertyAnimation(self.game_card, QByteArray(b"borderWidth"))
|
||||
self.thickness_anim.setDuration(self.theme.GAME_CARD_ANIMATION["thickness_anim_duration"])
|
||||
|
||||
def start_pulse_animation(self):
|
||||
"""Start pulse animation for border width when hovered or focused."""
|
||||
if not (self.game_card._hovered or self.game_card._focused):
|
||||
return
|
||||
if self.pulse_anim:
|
||||
self.pulse_anim.stop()
|
||||
self.pulse_anim = QPropertyAnimation(self.game_card, QByteArray(b"borderWidth"))
|
||||
self.pulse_anim.setDuration(self.theme.GAME_CARD_ANIMATION["pulse_anim_duration"])
|
||||
self.pulse_anim.setLoopCount(0)
|
||||
self.pulse_anim.setKeyValueAt(0, self.theme.GAME_CARD_ANIMATION["pulse_min_border_width"])
|
||||
self.pulse_anim.setKeyValueAt(0.5, self.theme.GAME_CARD_ANIMATION["pulse_max_border_width"])
|
||||
self.pulse_anim.setKeyValueAt(1, self.theme.GAME_CARD_ANIMATION["pulse_min_border_width"])
|
||||
self.pulse_anim.start()
|
||||
|
||||
def handle_enter_event(self):
|
||||
"""Handle mouse enter event animations."""
|
||||
self.game_card._hovered = True
|
||||
self.game_card.hoverChanged.emit(self.game_card.name, True)
|
||||
self.game_card.setFocus(Qt.FocusReason.MouseFocusReason)
|
||||
|
||||
if not self.thickness_anim:
|
||||
self.setup_animations()
|
||||
|
||||
if self.thickness_anim:
|
||||
self.thickness_anim.stop()
|
||||
if self._isPulseAnimationConnected:
|
||||
self.thickness_anim.finished.disconnect(self.start_pulse_animation)
|
||||
self._isPulseAnimationConnected = False
|
||||
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve"]]))
|
||||
self.thickness_anim.setStartValue(self.game_card._borderWidth)
|
||||
self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["hover_border_width"])
|
||||
self.thickness_anim.finished.connect(self.start_pulse_animation)
|
||||
self._isPulseAnimationConnected = True
|
||||
self.thickness_anim.start()
|
||||
|
||||
if self.gradient_anim:
|
||||
self.gradient_anim.stop()
|
||||
self.gradient_anim = QPropertyAnimation(self.game_card, QByteArray(b"gradientAngle"))
|
||||
self.gradient_anim.setDuration(self.theme.GAME_CARD_ANIMATION["gradient_anim_duration"])
|
||||
self.gradient_anim.setStartValue(self.theme.GAME_CARD_ANIMATION["gradient_start_angle"])
|
||||
self.gradient_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["gradient_end_angle"])
|
||||
self.gradient_anim.setLoopCount(-1)
|
||||
self.gradient_anim.start()
|
||||
|
||||
def handle_leave_event(self):
|
||||
"""Handle mouse leave event animations."""
|
||||
self.game_card._hovered = False
|
||||
self.game_card.hoverChanged.emit(self.game_card.name, False)
|
||||
if not self.game_card._focused:
|
||||
if self.gradient_anim:
|
||||
self.gradient_anim.stop()
|
||||
self.gradient_anim = None
|
||||
if self.pulse_anim:
|
||||
self.pulse_anim.stop()
|
||||
self.pulse_anim = None
|
||||
if self.thickness_anim:
|
||||
self.thickness_anim.stop()
|
||||
if self._isPulseAnimationConnected:
|
||||
self.thickness_anim.finished.disconnect(self.start_pulse_animation)
|
||||
self._isPulseAnimationConnected = False
|
||||
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve_out"]]))
|
||||
self.thickness_anim.setStartValue(self.game_card._borderWidth)
|
||||
self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["default_border_width"])
|
||||
self.thickness_anim.start()
|
||||
|
||||
def handle_focus_in_event(self):
|
||||
"""Handle focus in event animations."""
|
||||
if not self.game_card._hovered:
|
||||
self.game_card._focused = True
|
||||
self.game_card.focusChanged.emit(self.game_card.name, True)
|
||||
|
||||
if not self.thickness_anim:
|
||||
self.setup_animations()
|
||||
|
||||
if self.thickness_anim:
|
||||
self.thickness_anim.stop()
|
||||
if self._isPulseAnimationConnected:
|
||||
self.thickness_anim.finished.disconnect(self.start_pulse_animation)
|
||||
self._isPulseAnimationConnected = False
|
||||
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve"]]))
|
||||
self.thickness_anim.setStartValue(self.game_card._borderWidth)
|
||||
self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["focus_border_width"])
|
||||
self.thickness_anim.finished.connect(self.start_pulse_animation)
|
||||
self._isPulseAnimationConnected = True
|
||||
self.thickness_anim.start()
|
||||
|
||||
if self.gradient_anim:
|
||||
self.gradient_anim.stop()
|
||||
self.gradient_anim = QPropertyAnimation(self.game_card, QByteArray(b"gradientAngle"))
|
||||
self.gradient_anim.setDuration(self.theme.GAME_CARD_ANIMATION["gradient_anim_duration"])
|
||||
self.gradient_anim.setStartValue(self.theme.GAME_CARD_ANIMATION["gradient_start_angle"])
|
||||
self.gradient_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["gradient_end_angle"])
|
||||
self.gradient_anim.setLoopCount(-1)
|
||||
self.gradient_anim.start()
|
||||
|
||||
def handle_focus_out_event(self):
|
||||
"""Handle focus out event animations."""
|
||||
self.game_card._focused = False
|
||||
self.game_card.focusChanged.emit(self.game_card.name, False)
|
||||
if not self.game_card._hovered:
|
||||
if self.gradient_anim:
|
||||
self.gradient_anim.stop()
|
||||
self.gradient_anim = None
|
||||
if self.pulse_anim:
|
||||
self.pulse_anim.stop()
|
||||
self.pulse_anim = None
|
||||
if self.thickness_anim:
|
||||
self.thickness_anim.stop()
|
||||
if self._isPulseAnimationConnected:
|
||||
self.thickness_anim.finished.disconnect(self.start_pulse_animation)
|
||||
self._isPulseAnimationConnected = False
|
||||
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve_out"]]))
|
||||
self.thickness_anim.setStartValue(self.game_card._borderWidth)
|
||||
self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["default_border_width"])
|
||||
self.thickness_anim.start()
|
||||
|
||||
def paint_border(self, painter: QPainter):
|
||||
if not painter.isActive():
|
||||
logger.warning("Painter is not active; skipping border paint")
|
||||
return
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
pen = QPen()
|
||||
pen.setWidth(self.game_card._borderWidth)
|
||||
if self.game_card._hovered or self.game_card._focused:
|
||||
center = self.game_card.rect().center()
|
||||
gradient = QConicalGradient(center, self.game_card._gradientAngle)
|
||||
for stop in self.theme.GAME_CARD_ANIMATION["gradient_colors"]:
|
||||
gradient.setColorAt(stop["position"], QColor(stop["color"]))
|
||||
pen.setBrush(QBrush(gradient))
|
||||
else:
|
||||
pen.setColor(QColor(0, 0, 0, 0))
|
||||
painter.setPen(pen)
|
||||
radius = 18
|
||||
bw = round(self.game_card._borderWidth / 2)
|
||||
rect = self.game_card.rect().adjusted(bw, bw, -bw, -bw)
|
||||
if rect.isEmpty():
|
||||
return # Avoid drawing invalid rect
|
||||
painter.drawRoundedRect(rect, radius, radius)
|
||||
|
||||
class DetailPageAnimations:
|
||||
def __init__(self, main_window, theme=None):
|
||||
self.main_window = main_window
|
||||
self.theme = theme if theme is not None else default_styles
|
||||
self.animations = main_window._animations if hasattr(main_window, '_animations') else {}
|
||||
|
||||
def animate_detail_page(self, detail_page: QWidget, load_image_and_restore_effect: Callable, cleanup_animation: Callable):
|
||||
"""Animate the detail page based on theme settings."""
|
||||
animation_type = self.theme.GAME_CARD_ANIMATION.get("detail_page_animation_type", "fade")
|
||||
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_fade_duration", 350)
|
||||
|
||||
if animation_type == "fade":
|
||||
original_effect = detail_page.graphicsEffect()
|
||||
opacity_effect = SafeOpacityEffect(detail_page, disable_at_full=True)
|
||||
opacity_effect.setOpacity(0.0)
|
||||
detail_page.setGraphicsEffect(opacity_effect)
|
||||
animation = QPropertyAnimation(opacity_effect, QByteArray(b"opacity"))
|
||||
animation.setDuration(duration)
|
||||
animation.setStartValue(0.0)
|
||||
animation.setEndValue(0.999)
|
||||
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||
self.animations[detail_page] = animation
|
||||
def restore_effect():
|
||||
try:
|
||||
detail_page.setGraphicsEffect(original_effect) # type: ignore
|
||||
except RuntimeError:
|
||||
logger.debug("Original effect already deleted")
|
||||
animation.finished.connect(restore_effect)
|
||||
animation.finished.connect(load_image_and_restore_effect)
|
||||
animation.finished.connect(opacity_effect.deleteLater)
|
||||
elif animation_type in ["slide_left", "slide_right", "slide_up", "slide_down"]:
|
||||
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration", 500)
|
||||
easing_curve = QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION.get("detail_page_easing_curve", "OutCubic")])
|
||||
start_pos = {
|
||||
"slide_left": QPoint(self.main_window.width(), 0),
|
||||
"slide_right": QPoint(-self.main_window.width(), 0),
|
||||
"slide_up": QPoint(0, self.main_window.height()),
|
||||
"slide_down": QPoint(0, -self.main_window.height())
|
||||
}[animation_type]
|
||||
detail_page.move(start_pos)
|
||||
animation = QPropertyAnimation(detail_page, QByteArray(b"pos"))
|
||||
animation.setDuration(duration)
|
||||
animation.setStartValue(start_pos)
|
||||
animation.setEndValue(self.main_window.stackedWidget.rect().topLeft())
|
||||
animation.setEasingCurve(easing_curve)
|
||||
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||
self.animations[detail_page] = animation
|
||||
animation.finished.connect(cleanup_animation)
|
||||
animation.finished.connect(load_image_and_restore_effect)
|
||||
elif animation_type == "bounce":
|
||||
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_bounce_duration", 400)
|
||||
easing_curve = QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION.get("detail_page_easing_curve", "OutCubic")])
|
||||
detail_page.setWindowOpacity(0.0)
|
||||
opacity_anim = QPropertyAnimation(detail_page, QByteArray(b"windowOpacity"))
|
||||
opacity_anim.setDuration(duration)
|
||||
opacity_anim.setStartValue(0.0)
|
||||
opacity_anim.setEndValue(1.0)
|
||||
initial_rect = QRect(detail_page.x() + detail_page.width() // 4, detail_page.y() + detail_page.height() // 4,
|
||||
detail_page.width() // 2, detail_page.height() // 2)
|
||||
final_rect = detail_page.geometry()
|
||||
geometry_anim = QPropertyAnimation(detail_page, QByteArray(b"geometry"))
|
||||
geometry_anim.setDuration(duration)
|
||||
geometry_anim.setStartValue(initial_rect)
|
||||
geometry_anim.setEndValue(final_rect)
|
||||
geometry_anim.setEasingCurve(easing_curve)
|
||||
group_anim = QParallelAnimationGroup()
|
||||
group_anim.addAnimation(opacity_anim)
|
||||
group_anim.addAnimation(geometry_anim)
|
||||
group_anim.finished.connect(load_image_and_restore_effect)
|
||||
group_anim.finished.connect(cleanup_animation)
|
||||
group_anim.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||
self.animations[detail_page] = group_anim
|
||||
|
||||
def animate_detail_page_exit(self, detail_page: QWidget, cleanup_callback: Callable):
|
||||
"""Animate the detail page exit based on theme settings."""
|
||||
try:
|
||||
animation_type = self.theme.GAME_CARD_ANIMATION.get("detail_page_animation_type", "fade")
|
||||
|
||||
# Safely stop and remove any existing animation
|
||||
if detail_page in self.animations:
|
||||
try:
|
||||
animation = self.animations[detail_page]
|
||||
if isinstance(animation, QAbstractAnimation) and animation.state() == QAbstractAnimation.State.Running:
|
||||
animation.stop()
|
||||
except RuntimeError:
|
||||
logger.debug("Animation already deleted for page")
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping existing animation: {e}", exc_info=True)
|
||||
finally:
|
||||
self.animations.pop(detail_page, None)
|
||||
|
||||
# Define animation based on type
|
||||
if animation_type == "fade":
|
||||
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_fade_duration_exit", 350)
|
||||
original_effect = detail_page.graphicsEffect()
|
||||
opacity_effect = SafeOpacityEffect(detail_page, disable_at_full=False)
|
||||
opacity_effect.setOpacity(0.999)
|
||||
detail_page.setGraphicsEffect(opacity_effect)
|
||||
animation = QPropertyAnimation(opacity_effect, QByteArray(b"opacity"))
|
||||
animation.setDuration(duration)
|
||||
animation.setStartValue(0.999)
|
||||
animation.setEndValue(0.0)
|
||||
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||
self.animations[detail_page] = animation
|
||||
def restore_and_cleanup():
|
||||
try:
|
||||
detail_page.setGraphicsEffect(original_effect) # type: ignore
|
||||
except RuntimeError:
|
||||
logger.debug("Original effect already deleted")
|
||||
cleanup_callback()
|
||||
animation.finished.connect(restore_and_cleanup)
|
||||
animation.finished.connect(opacity_effect.deleteLater) # Clean up effect
|
||||
elif animation_type in ["slide_left", "slide_right", "slide_up", "slide_down"]:
|
||||
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration_exit", 500)
|
||||
easing_curve = QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION.get("detail_page_easing_curve_exit", "InCubic")])
|
||||
end_pos = {
|
||||
"slide_left": QPoint(-self.main_window.width(), 0), # Exit to left (opposite of entry)
|
||||
"slide_right": QPoint(self.main_window.width(), 0), # Exit to right
|
||||
"slide_up": QPoint(0, self.main_window.height()), # Exit downward
|
||||
"slide_down": QPoint(0, -self.main_window.height()) # Exit upward
|
||||
}[animation_type]
|
||||
animation = QPropertyAnimation(detail_page, QByteArray(b"pos"))
|
||||
animation.setDuration(duration)
|
||||
animation.setStartValue(detail_page.pos())
|
||||
animation.setEndValue(end_pos)
|
||||
animation.setEasingCurve(easing_curve)
|
||||
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||
self.animations[detail_page] = animation
|
||||
animation.finished.connect(cleanup_callback)
|
||||
elif animation_type == "bounce":
|
||||
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_bounce_duration_exit", 400)
|
||||
easing_curve = QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION.get("detail_page_easing_curve_exit", "InCubic")])
|
||||
opacity_anim = QPropertyAnimation(detail_page, QByteArray(b"windowOpacity"))
|
||||
opacity_anim.setDuration(duration)
|
||||
opacity_anim.setStartValue(1.0)
|
||||
opacity_anim.setEndValue(0.0)
|
||||
final_rect = QRect(detail_page.x() + detail_page.width() // 4, detail_page.y() + detail_page.height() // 4,
|
||||
detail_page.width() // 2, detail_page.height() // 2)
|
||||
geometry_anim = QPropertyAnimation(detail_page, QByteArray(b"geometry"))
|
||||
geometry_anim.setDuration(duration)
|
||||
geometry_anim.setStartValue(detail_page.geometry())
|
||||
geometry_anim.setEndValue(final_rect)
|
||||
geometry_anim.setEasingCurve(easing_curve)
|
||||
group_anim = QParallelAnimationGroup()
|
||||
group_anim.addAnimation(opacity_anim)
|
||||
group_anim.addAnimation(geometry_anim)
|
||||
group_anim.finished.connect(cleanup_callback)
|
||||
group_anim.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||
self.animations[detail_page] = group_anim
|
||||
except Exception as e:
|
||||
logger.error(f"Error in animate_detail_page_exit: {e}", exc_info=True)
|
||||
self.animations.pop(detail_page, None)
|
||||
cleanup_callback() # Fallback to cleanup if animation setup fails
|
@@ -1,12 +1,9 @@
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
from PySide6.QtCore import QLocale, QTranslator, QLibraryInfo
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from PySide6.QtGui import QIcon
|
||||
from portprotonqt.main_window import MainWindow
|
||||
from portprotonqt.tray import SystemTray
|
||||
from portprotonqt.config_utils import read_theme_from_config, save_fullscreen_config
|
||||
from portprotonqt.config_utils import save_fullscreen_config
|
||||
from portprotonqt.logger import get_logger
|
||||
from portprotonqt.cli import parse_args
|
||||
|
||||
@@ -14,7 +11,7 @@ logger = get_logger(__name__)
|
||||
|
||||
__app_id__ = "ru.linux_gaming.PortProtonQt"
|
||||
__app_name__ = "PortProtonQt"
|
||||
__app_version__ = "0.1.3"
|
||||
__app_version__ = "0.1.4"
|
||||
|
||||
def main():
|
||||
app = QApplication(sys.argv)
|
||||
@@ -35,47 +32,18 @@ def main():
|
||||
|
||||
window = MainWindow()
|
||||
|
||||
if args.session:
|
||||
gamescope_cmd = os.getenv("GAMESCOPE_CMD", "gamescope -f --xwayland-count 2")
|
||||
cmd = f"{gamescope_cmd} -- portprotonqt"
|
||||
logger.info(f"Executing: {cmd}")
|
||||
subprocess.Popen(cmd, shell=True)
|
||||
sys.exit(0)
|
||||
|
||||
if args.fullscreen:
|
||||
logger.info("Launching in fullscreen mode due to --fullscreen flag")
|
||||
save_fullscreen_config(True)
|
||||
window.showFullScreen()
|
||||
|
||||
current_theme_name = read_theme_from_config()
|
||||
tray = SystemTray(app, current_theme_name)
|
||||
tray.show_action.triggered.connect(window.show)
|
||||
tray.hide_action.triggered.connect(window.hide)
|
||||
|
||||
def recreate_tray():
|
||||
nonlocal tray
|
||||
if tray:
|
||||
logger.debug("Recreating system tray")
|
||||
tray.cleanup()
|
||||
tray = None
|
||||
current_theme = read_theme_from_config()
|
||||
tray = SystemTray(app, current_theme)
|
||||
# Ensure window is not None before connecting signals
|
||||
if window:
|
||||
tray.show_action.triggered.connect(window.show)
|
||||
tray.hide_action.triggered.connect(window.hide)
|
||||
|
||||
def cleanup_on_exit():
|
||||
nonlocal tray, window
|
||||
nonlocal window
|
||||
app.aboutToQuit.disconnect()
|
||||
if tray:
|
||||
tray.cleanup()
|
||||
tray = None
|
||||
if window:
|
||||
window.close()
|
||||
app.quit()
|
||||
|
||||
window.settings_saved.connect(recreate_tray)
|
||||
app.aboutToQuit.connect(cleanup_on_exit)
|
||||
|
||||
window.show()
|
||||
|
@@ -13,9 +13,4 @@ def parse_args():
|
||||
action="store_true",
|
||||
help="Запустить приложение в полноэкранном режиме и сохранить эту настройку"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--session",
|
||||
action="store_true",
|
||||
help="Запустить приложение с использованием gamescope"
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
@@ -148,10 +148,7 @@ class ContextMenuManager:
|
||||
return False
|
||||
current_exe = os.path.basename(exe_path)
|
||||
|
||||
# Check if the current_exe matches the target_exe in MainWindow
|
||||
if hasattr(self.parent, 'target_exe') and self.parent.target_exe == current_exe:
|
||||
return True
|
||||
return False
|
||||
return hasattr(self.parent, 'target_exe') and self.parent.target_exe == current_exe
|
||||
|
||||
def show_context_menu(self, game_card, pos: QPoint):
|
||||
"""
|
||||
@@ -161,7 +158,6 @@ class ContextMenuManager:
|
||||
game_card: The GameCard instance requesting the context menu.
|
||||
pos: The position (in widget coordinates) where the menu should appear.
|
||||
"""
|
||||
|
||||
def get_safe_icon(icon_name: str) -> QIcon:
|
||||
icon = self.theme_manager.get_icon(icon_name)
|
||||
if isinstance(icon, QIcon):
|
||||
@@ -173,7 +169,18 @@ class ContextMenuManager:
|
||||
menu = QMenu(self.parent)
|
||||
menu.setStyleSheet(self.theme.CONTEXT_MENU_STYLE)
|
||||
|
||||
# Check if the game is running
|
||||
# For non-Steam and non-Epic games, check if exe exists
|
||||
if game_card.game_source not in ("steam", "epic"):
|
||||
exec_line = self._get_exec_line(game_card.name, game_card.exec_line)
|
||||
exe_path = self._parse_exe_path(exec_line, game_card.name) if exec_line else None
|
||||
if not exe_path:
|
||||
# Show only "Delete from PortProton" if no valid exe
|
||||
delete_action = menu.addAction(get_safe_icon("delete"), _("Delete from PortProton"))
|
||||
delete_action.triggered.connect(lambda: self.delete_game(game_card.name, game_card.exec_line))
|
||||
menu.exec(game_card.mapToGlobal(pos))
|
||||
return
|
||||
|
||||
# Normal menu for games with valid exe or from Steam/Epic
|
||||
is_running = self._is_game_running(game_card)
|
||||
action_text = _("Stop Game") if is_running else _("Launch Game")
|
||||
action_icon = "stop" if is_running else "play"
|
||||
@@ -273,7 +280,12 @@ class ContextMenuManager:
|
||||
)
|
||||
)
|
||||
|
||||
menu.exec(game_card.mapToGlobal(pos))
|
||||
# Устанавливаем фокус на первый элемент меню
|
||||
actions = menu.actions()
|
||||
if actions:
|
||||
menu.setActiveAction(actions[0])
|
||||
|
||||
menu.exec(game_card.mapToGlobal(pos))
|
||||
|
||||
def _launch_game(self, game_card):
|
||||
"""
|
||||
@@ -697,15 +709,12 @@ Icon={icon_path}
|
||||
return None
|
||||
return exec_line
|
||||
|
||||
def _parse_exe_path(self, exec_line, game_name):
|
||||
def _parse_exe_path(self, exec_line: str, game_name: str) -> str | None:
|
||||
"""Parse the executable path from exec_line."""
|
||||
try:
|
||||
entry_exec_split = shlex.split(exec_line)
|
||||
if not entry_exec_split:
|
||||
self.signals.show_warning_dialog.emit(
|
||||
_("Error"),
|
||||
_("Invalid executable command: {exec_line}").format(exec_line=exec_line)
|
||||
)
|
||||
logger.debug("Invalid executable command for '%s': %s", game_name, exec_line)
|
||||
return None
|
||||
if entry_exec_split[0] == "env" and len(entry_exec_split) >= 3:
|
||||
exe_path = entry_exec_split[2]
|
||||
@@ -714,17 +723,11 @@ Icon={icon_path}
|
||||
else:
|
||||
exe_path = entry_exec_split[-1]
|
||||
if not exe_path or not os.path.exists(exe_path):
|
||||
self.signals.show_warning_dialog.emit(
|
||||
_("Error"),
|
||||
_("Executable not found: {path}").format(path=exe_path or "None")
|
||||
)
|
||||
logger.debug("Executable not found for '%s': %s", game_name, exe_path or "None")
|
||||
return None
|
||||
return exe_path
|
||||
except Exception as e:
|
||||
self.signals.show_warning_dialog.emit(
|
||||
_("Error"),
|
||||
_("Failed to parse executable: {error}").format(error=str(e))
|
||||
)
|
||||
logger.debug("Failed to parse executable for '%s': %s", game_name, e)
|
||||
return None
|
||||
|
||||
def _remove_file(self, file_path, error_message, success_message, game_name, location=""):
|
||||
|
Before Width: | Height: | Size: 447 KiB |
@@ -1,3 +0,0 @@
|
||||
name=Pulse Online
|
||||
description_ru=Многопользовательская онлайн-игра в жанре MMORPG, действие которой происходит в научно-фантастическом мире с уникальной боевой системой и глубоким крафтом. Игроки могут исследовать обширные локации, выполнять квесты, сражаться с противниками и взаимодействовать с другими участниками игры.
|
||||
description_en=A multiplayer online game in the MMORPG genre set in a sci-fi world with a unique combat system and deep crafting mechanics. Players can explore vast locations, complete quests, battle enemies, and interact with other participants in the game.
|
@@ -677,7 +677,10 @@ class AddGameDialog(QDialog):
|
||||
exe_path = self.exeEdit.text().strip()
|
||||
name = self.nameEdit.text().strip()
|
||||
|
||||
if not exe_path or not name:
|
||||
if not exe_path or not os.path.isfile(exe_path):
|
||||
return None, None
|
||||
|
||||
if not name:
|
||||
return None, None
|
||||
|
||||
portproton_path = get_portproton_location()
|
||||
|
@@ -144,14 +144,21 @@ class Downloader(QObject):
|
||||
logger.warning(f"Предыдущая ошибка загрузки для {url}, пропускаем")
|
||||
return None
|
||||
if url in self._cache:
|
||||
return self._cache[url]
|
||||
cached_path = self._cache[url]
|
||||
if os.path.exists(cached_path):
|
||||
if os.path.abspath(cached_path) == os.path.abspath(local_path):
|
||||
return cached_path
|
||||
else:
|
||||
del self._cache[url]
|
||||
url_lock = self._get_url_lock(url)
|
||||
with url_lock:
|
||||
with self._global_lock:
|
||||
if url in self._last_error:
|
||||
return None
|
||||
if url in self._cache:
|
||||
return self._cache[url]
|
||||
cached_path = self._cache[url]
|
||||
if os.path.exists(cached_path) and os.path.abspath(cached_path) == os.path.abspath(local_path):
|
||||
return cached_path
|
||||
result = download_with_cache(url, local_path, timeout, self)
|
||||
with self._global_lock:
|
||||
if result:
|
||||
|
@@ -16,13 +16,14 @@ from portprotonqt.time_utils import parse_playtime_file, format_playtime, get_la
|
||||
from portprotonqt.config_utils import get_portproton_location
|
||||
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
|
||||
search_app, get_steam_home, get_last_steam_user, convert_steam_id, generate_thumbnail, call_steam_api
|
||||
)
|
||||
import vdf
|
||||
import shutil
|
||||
import zlib
|
||||
from portprotonqt.downloader import Downloader
|
||||
from PySide6.QtGui import QPixmap
|
||||
import base64
|
||||
|
||||
logger = get_logger(__name__)
|
||||
downloader = Downloader()
|
||||
@@ -66,7 +67,8 @@ def get_cache_dir() -> Path:
|
||||
|
||||
def remove_egs_from_steam(game_name: str, portproton_dir: str, callback: Callable[[tuple[bool, str]], None]) -> None:
|
||||
"""
|
||||
Removes an EGS game from Steam by modifying shortcuts.vdf and deleting the launch script.
|
||||
Removes an EGS game from Steam using CEF API or by modifying shortcuts.vdf and deleting the launch script.
|
||||
Also deletes associated cover files in the Steam grid directory.
|
||||
Calls the callback with (success, message).
|
||||
|
||||
Args:
|
||||
@@ -74,6 +76,7 @@ def remove_egs_from_steam(game_name: str, portproton_dir: str, callback: Callabl
|
||||
portproton_dir: Path to the PortProton directory.
|
||||
callback: Callback function to handle the result (success, message).
|
||||
"""
|
||||
|
||||
if not portproton_dir:
|
||||
logger.error("PortProton directory not found")
|
||||
callback((False, "PortProton directory not found"))
|
||||
@@ -101,51 +104,89 @@ def remove_egs_from_steam(game_name: str, portproton_dir: str, callback: Callabl
|
||||
unsigned_id = convert_steam_id(user_id)
|
||||
user_dir = os.path.join(userdata_dir, str(unsigned_id))
|
||||
steam_shortcuts_path = os.path.join(user_dir, "config", "shortcuts.vdf")
|
||||
backup_path = f"{steam_shortcuts_path}.backup"
|
||||
grid_dir = os.path.join(user_dir, "config", "grid")
|
||||
|
||||
if not os.path.exists(steam_shortcuts_path):
|
||||
logger.error("Steam shortcuts file not found")
|
||||
callback((False, "Steam shortcuts file not found"))
|
||||
return
|
||||
|
||||
# Find appid for the shortcut
|
||||
try:
|
||||
with open(steam_shortcuts_path, 'rb') as f:
|
||||
shortcuts_data = vdf.binary_load(f)
|
||||
shortcuts = shortcuts_data.get("shortcuts", {})
|
||||
appid = None
|
||||
for _key, entry in shortcuts.items():
|
||||
if entry.get("AppName") == game_name and entry.get("Exe") == quoted_script_path:
|
||||
appid = convert_steam_id(int(entry.get("appid")))
|
||||
logger.info(f"Found matching shortcut for '{game_name}' with AppID {appid}")
|
||||
break
|
||||
if not appid:
|
||||
logger.info(f"Game '{game_name}' not found in Steam shortcuts")
|
||||
callback((False, f"Game '{game_name}' not found in Steam"))
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load shortcuts.vdf: {e}")
|
||||
callback((False, f"Failed to load shortcuts.vdf: {e}"))
|
||||
return
|
||||
|
||||
# Try CEF API first
|
||||
logger.info(f"Attempting to remove EGS game '{game_name}' via Steam CEF API with AppID {appid}")
|
||||
api_response = call_steam_api("removeShortcut", appid)
|
||||
if api_response is not None: # API responded, even if empty
|
||||
logger.info(f"Shortcut for AppID {appid} successfully removed via CEF API")
|
||||
|
||||
# Delete cover files
|
||||
cover_files = [
|
||||
os.path.join(grid_dir, f"{appid}.jpg"),
|
||||
os.path.join(grid_dir, f"{appid}p.jpg"),
|
||||
os.path.join(grid_dir, f"{appid}_hero.jpg"),
|
||||
os.path.join(grid_dir, f"{appid}_logo.png")
|
||||
]
|
||||
for cover_file in cover_files:
|
||||
if os.path.exists(cover_file):
|
||||
try:
|
||||
os.remove(cover_file)
|
||||
logger.info(f"Deleted cover file: {cover_file}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete cover file {cover_file}: {e}")
|
||||
|
||||
# Delete launch script
|
||||
if os.path.exists(script_path):
|
||||
try:
|
||||
os.remove(script_path)
|
||||
logger.info(f"Removed EGS script: {script_path}")
|
||||
except OSError as e:
|
||||
logger.warning(f"Failed to remove EGS script '{script_path}': {str(e)}")
|
||||
|
||||
callback((True, f"Game '{game_name}' was removed from Steam. Please restart Steam for changes to take effect."))
|
||||
return
|
||||
|
||||
# Fallback to VDF modification
|
||||
logger.warning("CEF API failed for EGS game removal; falling back to VDF modification")
|
||||
backup_path = f"{steam_shortcuts_path}.backup"
|
||||
try:
|
||||
shutil.copy2(steam_shortcuts_path, backup_path)
|
||||
logger.info("Created backup of shortcuts.vdf at %s", backup_path)
|
||||
logger.info(f"Created backup of shortcuts.vdf at {backup_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create backup of shortcuts.vdf: {e}")
|
||||
callback((False, f"Failed to create backup of shortcuts.vdf: {e}"))
|
||||
return
|
||||
|
||||
try:
|
||||
with open(steam_shortcuts_path, 'rb') as f:
|
||||
shortcuts_data = vdf.binary_load(f)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load shortcuts.vdf: {e}")
|
||||
callback((False, f"Failed to load shortcuts.vdf: {e}"))
|
||||
return
|
||||
new_shortcuts = {}
|
||||
index = 0
|
||||
for _key, entry in shortcuts.items():
|
||||
if entry.get("AppName") == game_name and entry.get("Exe") == quoted_script_path:
|
||||
logger.info(f"Removing EGS game '{game_name}' from Steam shortcuts")
|
||||
continue
|
||||
new_shortcuts[str(index)] = entry
|
||||
index += 1
|
||||
|
||||
shortcuts = shortcuts_data.get("shortcuts", {})
|
||||
modified = False
|
||||
new_shortcuts = {}
|
||||
index = 0
|
||||
|
||||
for _key, entry in shortcuts.items():
|
||||
if entry.get("AppName") == game_name and entry.get("Exe") == quoted_script_path:
|
||||
modified = True
|
||||
logger.info("Removing EGS game '%s' from Steam shortcuts", game_name)
|
||||
continue
|
||||
new_shortcuts[str(index)] = entry
|
||||
index += 1
|
||||
|
||||
if not modified:
|
||||
logger.error("Game '%s' not found in Steam shortcuts", game_name)
|
||||
callback((False, f"Game '{game_name}' not found in Steam shortcuts"))
|
||||
return
|
||||
|
||||
try:
|
||||
with open(steam_shortcuts_path, 'wb') as f:
|
||||
vdf.binary_dump({"shortcuts": new_shortcuts}, f)
|
||||
logger.info("Updated shortcuts.vdf, removed '%s'", game_name)
|
||||
logger.info(f"Updated shortcuts.vdf, removed '{game_name}'")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update shortcuts.vdf: {e}")
|
||||
if os.path.exists(backup_path):
|
||||
@@ -157,10 +198,26 @@ def remove_egs_from_steam(game_name: str, portproton_dir: str, callback: Callabl
|
||||
callback((False, f"Failed to update shortcuts.vdf: {e}"))
|
||||
return
|
||||
|
||||
# Delete cover files
|
||||
cover_files = [
|
||||
os.path.join(grid_dir, f"{appid}.jpg"),
|
||||
os.path.join(grid_dir, f"{appid}p.jpg"),
|
||||
os.path.join(grid_dir, f"{appid}_hero.jpg"),
|
||||
os.path.join(grid_dir, f"{appid}_logo.png")
|
||||
]
|
||||
for cover_file in cover_files:
|
||||
if os.path.exists(cover_file):
|
||||
try:
|
||||
os.remove(cover_file)
|
||||
logger.info(f"Deleted cover file: {cover_file}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete cover file {cover_file}: {e}")
|
||||
|
||||
# Delete launch script
|
||||
if os.path.exists(script_path):
|
||||
try:
|
||||
os.remove(script_path)
|
||||
logger.info("Removed EGS script: %s", script_path)
|
||||
logger.info(f"Removed EGS script: {script_path}")
|
||||
except OSError as e:
|
||||
logger.warning(f"Failed to remove EGS script '{script_path}': {str(e)}")
|
||||
|
||||
@@ -168,11 +225,17 @@ def remove_egs_from_steam(game_name: str, portproton_dir: str, callback: Callabl
|
||||
|
||||
def add_egs_to_steam(app_name: str, game_title: str, legendary_path: str, callback: Callable[[tuple[bool, str]], None]) -> None:
|
||||
"""
|
||||
Asynchronously adds an EGS game to Steam via shortcuts.vdf with PortProton tag.
|
||||
Asynchronously adds an EGS game to Steam via CEF API or shortcuts.vdf with PortProton tag.
|
||||
Creates a launch script using legendary CLI with --no-wine and PortProton wrapper.
|
||||
Wrapper is flatpak run if portproton_location is None or contains .var, otherwise start.sh.
|
||||
Downloads Steam Grid covers if the game exists in Steam, and generates a thumbnail.
|
||||
Calls the callback with (success, message).
|
||||
|
||||
Args:
|
||||
app_name: The Legendary app_name (unique identifier for the game).
|
||||
game_title: The display name of the game.
|
||||
legendary_path: Path to the Legendary CLI executable.
|
||||
callback: Callback function to handle the result (success, message).
|
||||
"""
|
||||
if not app_name or not app_name.strip() or not game_title or not game_title.strip():
|
||||
logger.error("Invalid app_name or game_title: empty or whitespace")
|
||||
@@ -267,47 +330,47 @@ export LEGENDARY_CONFIG_PATH="{legendary_config_path}"
|
||||
grid_dir = user_dir / "config" / "grid"
|
||||
os.makedirs(grid_dir, exist_ok=True)
|
||||
|
||||
# Backup shortcuts.vdf
|
||||
backup_path = f"{steam_shortcuts_path}.backup"
|
||||
if os.path.exists(steam_shortcuts_path):
|
||||
try:
|
||||
shutil.copy2(steam_shortcuts_path, backup_path)
|
||||
logger.info(f"Created backup of shortcuts.vdf at {backup_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create backup of shortcuts.vdf: {e}")
|
||||
callback((False, f"Failed to create backup of shortcuts.vdf: {e}"))
|
||||
return
|
||||
# Try CEF API first
|
||||
logger.info(f"Attempting to add EGS game '{game_title}' via Steam CEF API")
|
||||
api_response = call_steam_api(
|
||||
"createShortcut",
|
||||
game_title,
|
||||
script_path,
|
||||
str(Path(script_path).parent),
|
||||
icon_path,
|
||||
""
|
||||
)
|
||||
|
||||
# Generate unique appid
|
||||
unique_string = f"{script_path}{game_title}"
|
||||
baseid = zlib.crc32(unique_string.encode('utf-8')) & 0xffffffff
|
||||
appid = baseid | 0x80000000
|
||||
if appid > 0x7FFFFFFF:
|
||||
aidvdf = appid - 0x100000000
|
||||
appid = None
|
||||
was_api_used = False
|
||||
|
||||
if api_response and isinstance(api_response, dict) and 'id' in api_response:
|
||||
appid = api_response['id']
|
||||
was_api_used = True
|
||||
logger.info(f"EGS game '{game_title}' successfully added via CEF API. AppID: {appid}")
|
||||
else:
|
||||
aidvdf = appid
|
||||
logger.warning("CEF API failed for EGS game addition; falling back to VDF modification")
|
||||
# Backup shortcuts.vdf
|
||||
backup_path = f"{steam_shortcuts_path}.backup"
|
||||
if os.path.exists(steam_shortcuts_path):
|
||||
try:
|
||||
shutil.copy2(steam_shortcuts_path, backup_path)
|
||||
logger.info(f"Created backup of shortcuts.vdf at {backup_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create backup of shortcuts.vdf: {e}")
|
||||
callback((False, f"Failed to create backup of shortcuts.vdf: {e}"))
|
||||
return
|
||||
|
||||
steam_appid = None
|
||||
downloaded_count = 0
|
||||
total_covers = 4
|
||||
download_lock = threading.Lock()
|
||||
# Generate unique appid
|
||||
unique_string = f"{script_path}{game_title}"
|
||||
baseid = zlib.crc32(unique_string.encode('utf-8')) & 0xffffffff
|
||||
appid = baseid | 0x80000000
|
||||
if appid > 0x7FFFFFFF:
|
||||
aidvdf = appid - 0x100000000
|
||||
else:
|
||||
aidvdf = appid
|
||||
|
||||
def on_cover_download(cover_file: str, cover_type: str):
|
||||
nonlocal downloaded_count
|
||||
try:
|
||||
if cover_file and os.path.exists(cover_file):
|
||||
logger.info(f"Downloaded cover {cover_type} to {cover_file}")
|
||||
else:
|
||||
logger.warning(f"Failed to download cover {cover_type} for appid {steam_appid}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing cover {cover_type} for appid {steam_appid}: {e}")
|
||||
with download_lock:
|
||||
downloaded_count += 1
|
||||
if downloaded_count == total_covers:
|
||||
finalize_shortcut()
|
||||
|
||||
def finalize_shortcut():
|
||||
tags_dict = {'0': 'PortProton'}
|
||||
# Create shortcut entry
|
||||
shortcut = {
|
||||
"appid": aidvdf,
|
||||
"AppName": game_title,
|
||||
@@ -322,7 +385,7 @@ export LEGENDARY_CONFIG_PATH="{legendary_config_path}"
|
||||
"Devkit": 0,
|
||||
"DevkitGameID": "",
|
||||
"LastPlayTime": 0,
|
||||
"tags": tags_dict
|
||||
"tags": {'0': 'PortProton'}
|
||||
}
|
||||
logger.info(f"Shortcut entry for EGS game: {shortcut}")
|
||||
|
||||
@@ -353,6 +416,7 @@ export LEGENDARY_CONFIG_PATH="{legendary_config_path}"
|
||||
|
||||
with open(steam_shortcuts_path, 'wb') as f:
|
||||
vdf.binary_dump({"shortcuts": shortcuts}, f)
|
||||
logger.info(f"EGS game '{game_title}' added to Steam via VDF")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update shortcuts.vdf: {e}")
|
||||
if os.path.exists(backup_path):
|
||||
@@ -364,8 +428,42 @@ export LEGENDARY_CONFIG_PATH="{legendary_config_path}"
|
||||
callback((False, f"Failed to update shortcuts.vdf: {e}"))
|
||||
return
|
||||
|
||||
logger.info(f"EGS game '{game_title}' added to Steam")
|
||||
callback((True, f"Game '{game_title}' added to Steam with covers"))
|
||||
if not appid:
|
||||
callback((False, "Failed to create shortcut via any method"))
|
||||
return
|
||||
|
||||
steam_appid = None
|
||||
downloaded_count = 0
|
||||
total_covers = 4
|
||||
download_lock = threading.Lock()
|
||||
|
||||
def on_cover_download(cover_file: str | None, cover_type: str, index: int):
|
||||
nonlocal downloaded_count
|
||||
try:
|
||||
if cover_file is None or not os.path.exists(cover_file):
|
||||
logger.warning(f"Failed to download cover {cover_type} for appid {steam_appid}")
|
||||
with download_lock:
|
||||
downloaded_count += 1
|
||||
if downloaded_count == total_covers:
|
||||
callback((True, f"Game '{game_title}' added to Steam with covers"))
|
||||
return
|
||||
|
||||
logger.info(f"Downloaded cover {cover_type} to {cover_file}")
|
||||
if was_api_used:
|
||||
try:
|
||||
with open(cover_file, 'rb') as f:
|
||||
img_b64 = base64.b64encode(f.read()).decode('utf-8')
|
||||
logger.info(f"Applying cover type '{cover_type}' via API for AppID {appid}")
|
||||
ext = Path(cover_type).suffix.lstrip('.')
|
||||
call_steam_api("setGrid", appid, index, ext, img_b64)
|
||||
except Exception as e:
|
||||
logger.error(f"Error applying cover '{cover_type}' via API: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing cover {cover_type} for appid {steam_appid}: {e}")
|
||||
with download_lock:
|
||||
downloaded_count += 1
|
||||
if downloaded_count == total_covers:
|
||||
callback((True, f"Game '{game_title}' added to Steam with covers"))
|
||||
|
||||
def on_steam_apps(steam_data: tuple[list, dict]):
|
||||
nonlocal steam_appid
|
||||
@@ -375,24 +473,24 @@ export LEGENDARY_CONFIG_PATH="{legendary_config_path}"
|
||||
|
||||
if not steam_appid:
|
||||
logger.info(f"No Steam appid found for EGS game {game_title}, skipping cover download")
|
||||
finalize_shortcut()
|
||||
callback((True, f"Game '{game_title}' added to Steam"))
|
||||
return
|
||||
|
||||
cover_types = [
|
||||
(".jpg", "header.jpg"),
|
||||
("p.jpg", "library_600x900_2x.jpg"),
|
||||
("_hero.jpg", "library_hero.jpg"),
|
||||
("_logo.png", "logo.png")
|
||||
(".jpg", "header.jpg", 0),
|
||||
("p.jpg", "library_600x900_2x.jpg", 1),
|
||||
("_hero.jpg", "library_hero.jpg", 2),
|
||||
("_logo.png", "logo.png", 3)
|
||||
]
|
||||
|
||||
for suffix, cover_type in cover_types:
|
||||
for suffix, cover_type, index in cover_types:
|
||||
cover_file = os.path.join(grid_dir, f"{appid}{suffix}")
|
||||
cover_url = f"https://cdn.cloudflare.steamstatic.com/steam/apps/{steam_appid}/{cover_type}"
|
||||
downloader.download_async(
|
||||
cover_url,
|
||||
cover_file,
|
||||
timeout=5,
|
||||
callback=lambda result, cfile=cover_file, ctype=cover_type: on_cover_download(cfile, ctype)
|
||||
callback=lambda result, ctype=cover_type, idx=index: on_cover_download(result, ctype, idx)
|
||||
)
|
||||
|
||||
get_steam_apps_and_index_async(on_steam_apps)
|
||||
@@ -747,6 +845,11 @@ def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tu
|
||||
games: list[tuple] = []
|
||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
user_json_path = cache_dir / "user.json"
|
||||
if not user_json_path.exists():
|
||||
callback(games)
|
||||
return
|
||||
|
||||
def process_games(installed_games: list | None):
|
||||
if installed_games is None:
|
||||
logger.info("No installed Epic Games Store games found")
|
||||
@@ -855,12 +958,12 @@ def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tu
|
||||
app_name,
|
||||
f"legendary:launch:{app_name}",
|
||||
"",
|
||||
last_launch, # Время последнего запуска
|
||||
formatted_playtime, # Форматированное время игры
|
||||
protondb_tier, # ProtonDB tier
|
||||
last_launch,
|
||||
formatted_playtime,
|
||||
protondb_tier,
|
||||
status or "",
|
||||
last_launch_timestamp, # Временная метка последнего запуска
|
||||
playtime_seconds, # Время игры в секундах
|
||||
last_launch_timestamp,
|
||||
playtime_seconds,
|
||||
"epic"
|
||||
)
|
||||
pending_images -= 1
|
||||
@@ -880,7 +983,7 @@ def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tu
|
||||
get_protondb_tier_async(steam_appid, on_protondb_tier)
|
||||
else:
|
||||
logger.debug(f"No Steam app found for EGS game {title}")
|
||||
on_protondb_tier("") # Proceed with empty ProtonDB tier
|
||||
on_protondb_tier("")
|
||||
|
||||
get_steam_apps_and_index_async(on_steam_apps)
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
from PySide6.QtGui import QPainter, QPen, QColor, QConicalGradient, QBrush, QDesktopServices
|
||||
from PySide6.QtCore import QEasingCurve, Signal, Property, Qt, QPropertyAnimation, QByteArray, QUrl
|
||||
from PySide6.QtGui import QPainter, QColor, QDesktopServices
|
||||
from PySide6.QtCore import Signal, Property, Qt, QUrl
|
||||
from PySide6.QtWidgets import QFrame, QGraphicsDropShadowEffect, QVBoxLayout, QWidget, QStackedLayout, QLabel
|
||||
from collections.abc import Callable
|
||||
import portprotonqt.themes.standart.styles as default_styles
|
||||
@@ -11,9 +11,11 @@ from portprotonqt.config_utils import read_theme_from_config
|
||||
from portprotonqt.custom_widgets import ClickableLabel
|
||||
from portprotonqt.portproton_api import PortProtonAPI
|
||||
from portprotonqt.downloader import Downloader
|
||||
from portprotonqt.animations import GameCardAnimations
|
||||
import weakref
|
||||
from typing import cast
|
||||
|
||||
|
||||
class GameCard(QFrame):
|
||||
borderWidthChanged = Signal()
|
||||
gradientAngleChanged = Signal()
|
||||
@@ -78,13 +80,8 @@ class GameCard(QFrame):
|
||||
self._focused = False
|
||||
|
||||
# Анимации
|
||||
self.thickness_anim = QPropertyAnimation(self, QByteArray(b"borderWidth"))
|
||||
self.thickness_anim.setDuration(self.theme.GAME_CARD_ANIMATION["thickness_anim_duration"])
|
||||
self.gradient_anim = None
|
||||
self.pulse_anim = None
|
||||
|
||||
# Флаг для отслеживания подключения слота startPulseAnimation
|
||||
self._isPulseAnimationConnected = False
|
||||
self.animations = GameCardAnimations(self, self.theme)
|
||||
self.animations.setup_animations()
|
||||
|
||||
# Тень
|
||||
shadow = QGraphicsDropShadowEffect(self)
|
||||
@@ -191,7 +188,7 @@ class GameCard(QFrame):
|
||||
self.egsLabel.setVisible(self.egs_visible)
|
||||
|
||||
# PortProton бейдж
|
||||
portproton_icon = self.theme_manager.get_icon("ppqt-tray")
|
||||
portproton_icon = self.theme_manager.get_icon("portproton")
|
||||
self.portprotonLabel = ClickableLabel(
|
||||
"PortProton",
|
||||
icon=portproton_icon,
|
||||
@@ -455,133 +452,22 @@ class GameCard(QFrame):
|
||||
|
||||
def paintEvent(self, event):
|
||||
super().paintEvent(event)
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
|
||||
pen = QPen()
|
||||
pen.setWidth(self._borderWidth)
|
||||
if self._hovered or self._focused:
|
||||
center = self.rect().center()
|
||||
gradient = QConicalGradient(center, self._gradientAngle)
|
||||
for stop in self.theme.GAME_CARD_ANIMATION["gradient_colors"]:
|
||||
gradient.setColorAt(stop["position"], QColor(stop["color"]))
|
||||
pen.setBrush(QBrush(gradient))
|
||||
else:
|
||||
pen.setColor(QColor(0, 0, 0, 0))
|
||||
|
||||
painter.setPen(pen)
|
||||
radius = 18
|
||||
bw = round(self._borderWidth / 2)
|
||||
rect = self.rect().adjusted(bw, bw, -bw, -bw)
|
||||
painter.drawRoundedRect(rect, radius, radius)
|
||||
|
||||
def startPulseAnimation(self):
|
||||
if not (self._hovered or self._focused):
|
||||
return
|
||||
if self.pulse_anim:
|
||||
self.pulse_anim.stop()
|
||||
self.pulse_anim = QPropertyAnimation(self, QByteArray(b"borderWidth"))
|
||||
self.pulse_anim.setDuration(self.theme.GAME_CARD_ANIMATION["pulse_anim_duration"])
|
||||
self.pulse_anim.setLoopCount(0)
|
||||
self.pulse_anim.setKeyValueAt(0, self.theme.GAME_CARD_ANIMATION["pulse_min_border_width"])
|
||||
self.pulse_anim.setKeyValueAt(0.5, self.theme.GAME_CARD_ANIMATION["pulse_max_border_width"])
|
||||
self.pulse_anim.setKeyValueAt(1, self.theme.GAME_CARD_ANIMATION["pulse_min_border_width"])
|
||||
self.pulse_anim.start()
|
||||
self.animations.paint_border(QPainter(self))
|
||||
|
||||
def enterEvent(self, event):
|
||||
self._hovered = True
|
||||
self.hoverChanged.emit(self.name, True)
|
||||
self.setFocus(Qt.FocusReason.MouseFocusReason)
|
||||
|
||||
self.thickness_anim.stop()
|
||||
if self._isPulseAnimationConnected:
|
||||
self.thickness_anim.finished.disconnect(self.startPulseAnimation)
|
||||
self._isPulseAnimationConnected = False
|
||||
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve"]]))
|
||||
self.thickness_anim.setStartValue(self._borderWidth)
|
||||
self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["hover_border_width"])
|
||||
self.thickness_anim.finished.connect(self.startPulseAnimation)
|
||||
self._isPulseAnimationConnected = True
|
||||
self.thickness_anim.start()
|
||||
|
||||
if self.gradient_anim:
|
||||
self.gradient_anim.stop()
|
||||
self.gradient_anim = QPropertyAnimation(self, QByteArray(b"gradientAngle"))
|
||||
self.gradient_anim.setDuration(self.theme.GAME_CARD_ANIMATION["gradient_anim_duration"])
|
||||
self.gradient_anim.setStartValue(self.theme.GAME_CARD_ANIMATION["gradient_start_angle"])
|
||||
self.gradient_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["gradient_end_angle"])
|
||||
self.gradient_anim.setLoopCount(-1)
|
||||
self.gradient_anim.start()
|
||||
|
||||
self.animations.handle_enter_event()
|
||||
super().enterEvent(event)
|
||||
|
||||
def leaveEvent(self, event):
|
||||
self._hovered = False
|
||||
self.hoverChanged.emit(self.name, False)
|
||||
if not self._focused:
|
||||
if self.gradient_anim:
|
||||
self.gradient_anim.stop()
|
||||
self.gradient_anim = None
|
||||
if self.pulse_anim:
|
||||
self.pulse_anim.stop()
|
||||
self.pulse_anim = None
|
||||
if self.thickness_anim:
|
||||
self.thickness_anim.stop()
|
||||
if self._isPulseAnimationConnected:
|
||||
self.thickness_anim.finished.disconnect(self.startPulseAnimation)
|
||||
self._isPulseAnimationConnected = False
|
||||
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve_out"]]))
|
||||
self.thickness_anim.setStartValue(self._borderWidth)
|
||||
self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["default_border_width"])
|
||||
self.thickness_anim.start()
|
||||
self.animations.handle_leave_event()
|
||||
super().leaveEvent(event)
|
||||
|
||||
def focusInEvent(self, event):
|
||||
if not self._hovered:
|
||||
self._focused = True
|
||||
self.focusChanged.emit(self.name, True)
|
||||
|
||||
self.thickness_anim.stop()
|
||||
if self._isPulseAnimationConnected:
|
||||
self.thickness_anim.finished.disconnect(self.startPulseAnimation)
|
||||
self._isPulseAnimationConnected = False
|
||||
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve"]]))
|
||||
self.thickness_anim.setStartValue(self._borderWidth)
|
||||
self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["focus_border_width"])
|
||||
self.thickness_anim.finished.connect(self.startPulseAnimation)
|
||||
self._isPulseAnimationConnected = True
|
||||
self.thickness_anim.start()
|
||||
|
||||
if self.gradient_anim:
|
||||
self.gradient_anim.stop()
|
||||
self.gradient_anim = QPropertyAnimation(self, QByteArray(b"gradientAngle"))
|
||||
self.gradient_anim.setDuration(self.theme.GAME_CARD_ANIMATION["gradient_anim_duration"])
|
||||
self.gradient_anim.setStartValue(self.theme.GAME_CARD_ANIMATION["gradient_start_angle"])
|
||||
self.gradient_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["gradient_end_angle"])
|
||||
self.gradient_anim.setLoopCount(-1)
|
||||
self.gradient_anim.start()
|
||||
|
||||
self.animations.handle_focus_in_event()
|
||||
super().focusInEvent(event)
|
||||
|
||||
def focusOutEvent(self, event):
|
||||
self._focused = False
|
||||
self.focusChanged.emit(self.name, False)
|
||||
if not self._hovered:
|
||||
if self.gradient_anim:
|
||||
self.gradient_anim.stop()
|
||||
self.gradient_anim = None
|
||||
if self.pulse_anim:
|
||||
self.pulse_anim.stop()
|
||||
self.pulse_anim = None
|
||||
if self.thickness_anim:
|
||||
self.thickness_anim.stop()
|
||||
if self._isPulseAnimationConnected:
|
||||
self.thickness_anim.finished.disconnect(self.startPulseAnimation)
|
||||
self._isPulseAnimationConnected = False
|
||||
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve_out"]]))
|
||||
self.thickness_anim.setStartValue(self._borderWidth)
|
||||
self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["default_border_width"])
|
||||
self.thickness_anim.start()
|
||||
self.animations.handle_focus_out_event()
|
||||
super().focusOutEvent(event)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
|
373
portprotonqt/howlongtobeat_api.py
Normal file
@@ -0,0 +1,373 @@
|
||||
import orjson
|
||||
import re
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
from difflib import SequenceMatcher
|
||||
from threading import Thread
|
||||
import requests
|
||||
from bs4 import BeautifulSoup, Tag
|
||||
from portprotonqt.config_utils import read_proxy_config
|
||||
from portprotonqt.time_utils import format_playtime
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
|
||||
@dataclass
|
||||
class GameEntry:
|
||||
"""Информация об игре из HowLongToBeat."""
|
||||
game_id: int = -1
|
||||
game_name: str | None = None
|
||||
main_story: float | None = None
|
||||
main_extra: float | None = None
|
||||
completionist: float | None = None
|
||||
similarity: float = -1.0
|
||||
raw_data: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@dataclass
|
||||
class SearchConfig:
|
||||
"""Конфигурация для поиска."""
|
||||
api_key: str | None = None
|
||||
search_url: str | None = None
|
||||
|
||||
class APIKeyExtractor:
|
||||
"""Извлекает API ключ и URL поиска из скриптов сайта."""
|
||||
@staticmethod
|
||||
def extract_from_script(script_content: str) -> SearchConfig:
|
||||
config = SearchConfig()
|
||||
config.api_key = APIKeyExtractor._extract_api_key(script_content)
|
||||
config.search_url = APIKeyExtractor._extract_search_url(script_content, config.api_key)
|
||||
return config
|
||||
|
||||
@staticmethod
|
||||
def _extract_api_key(script_content: str) -> str | None:
|
||||
user_id_pattern = r'users\s*:\s*{\s*id\s*:\s*"([^"]+)"'
|
||||
matches = re.findall(user_id_pattern, script_content)
|
||||
if matches:
|
||||
return ''.join(matches)
|
||||
concat_pattern = r'\/api\/\w+\/"(?:\.concat\("[^"]*"\))+'
|
||||
matches = re.findall(concat_pattern, script_content)
|
||||
if matches:
|
||||
parts = str(matches).split('.concat')
|
||||
cleaned_parts = [re.sub(r'["\(\)\[\]\']', '', part) for part in parts[1:]]
|
||||
return ''.join(cleaned_parts)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _extract_search_url(script_content: str, api_key: str | None) -> str | None:
|
||||
if not api_key:
|
||||
return None
|
||||
pattern = re.compile(
|
||||
r'fetch\(\s*["\'](\/api\/[^"\']*)["\']'
|
||||
r'((?:\s*\.concat\(\s*["\']([^"\']*)["\']\s*\))+)'
|
||||
r'\s*,',
|
||||
re.DOTALL
|
||||
)
|
||||
for match in pattern.finditer(script_content):
|
||||
endpoint = match.group(1)
|
||||
concat_calls = match.group(2)
|
||||
concat_strings = re.findall(r'\.concat\(\s*["\']([^"\']*)["\']\s*\)', concat_calls)
|
||||
concatenated_str = ''.join(concat_strings)
|
||||
if concatenated_str == api_key:
|
||||
return endpoint
|
||||
return None
|
||||
|
||||
class HTTPClient:
|
||||
"""HTTP клиент для работы с API HowLongToBeat."""
|
||||
BASE_URL = 'https://howlongtobeat.com/'
|
||||
SEARCH_URL = BASE_URL + "api/s/"
|
||||
|
||||
def __init__(self, timeout: int = 60):
|
||||
self.timeout = timeout
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'referer': self.BASE_URL
|
||||
})
|
||||
proxy_config = read_proxy_config()
|
||||
if proxy_config:
|
||||
self.session.proxies.update(proxy_config)
|
||||
|
||||
def get_search_config(self, parse_all_scripts: bool = False) -> SearchConfig | None:
|
||||
try:
|
||||
response = self.session.get(self.BASE_URL, timeout=self.timeout)
|
||||
response.raise_for_status()
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
scripts = soup.find_all('script', src=True)
|
||||
script_urls = []
|
||||
for script in scripts:
|
||||
if isinstance(script, Tag):
|
||||
src = script.get('src')
|
||||
if src is not None and isinstance(src, str):
|
||||
if parse_all_scripts or '_app-' in src:
|
||||
script_urls.append(src)
|
||||
for script_url in script_urls:
|
||||
full_url = self.BASE_URL + script_url
|
||||
script_response = self.session.get(full_url, timeout=self.timeout)
|
||||
if script_response.status_code == 200:
|
||||
config = APIKeyExtractor.extract_from_script(script_response.text)
|
||||
if config.api_key:
|
||||
return config
|
||||
except requests.RequestException:
|
||||
pass
|
||||
return None
|
||||
|
||||
def search_games(self, game_name: str, page: int = 1, config: SearchConfig | None = None) -> str | None:
|
||||
if not config:
|
||||
config = self.get_search_config()
|
||||
if not config:
|
||||
config = self.get_search_config(parse_all_scripts=True)
|
||||
if not config or not config.api_key:
|
||||
return None
|
||||
search_url = self.SEARCH_URL
|
||||
if config.search_url:
|
||||
search_url = self.BASE_URL + config.search_url.lstrip('/')
|
||||
payload = self._build_search_payload(game_name, page, config)
|
||||
headers = {
|
||||
'content-type': 'application/json',
|
||||
'accept': '*/*'
|
||||
}
|
||||
try:
|
||||
response = self.session.post(
|
||||
search_url + config.api_key,
|
||||
headers=headers,
|
||||
data=orjson.dumps(payload),
|
||||
timeout=self.timeout
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return response.text
|
||||
except requests.RequestException:
|
||||
pass
|
||||
try:
|
||||
response = self.session.post(
|
||||
search_url,
|
||||
headers=headers,
|
||||
data=orjson.dumps(payload),
|
||||
timeout=self.timeout
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return response.text
|
||||
except requests.RequestException:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _build_search_payload(self, game_name: str, page: int, config: SearchConfig) -> dict[str, Any]:
|
||||
payload = {
|
||||
'searchType': "games",
|
||||
'searchTerms': game_name.split(),
|
||||
'searchPage': page,
|
||||
'size': 1, # Limit to 1 result
|
||||
'searchOptions': {
|
||||
'games': {
|
||||
'userId': 0,
|
||||
'platform': "",
|
||||
'sortCategory': "popular",
|
||||
'rangeCategory': "main",
|
||||
'rangeTime': {'min': 0, 'max': 0},
|
||||
'gameplay': {
|
||||
'perspective': "",
|
||||
'flow': "",
|
||||
'genre': "",
|
||||
"difficulty": ""
|
||||
},
|
||||
'rangeYear': {'max': "", 'min': ""},
|
||||
'modifier': "" # Hardcoded to empty string for SearchModifiers.NONE
|
||||
},
|
||||
'users': {'sortCategory': "postcount"},
|
||||
'lists': {'sortCategory': "follows"},
|
||||
'filter': "",
|
||||
'sort': 0,
|
||||
'randomizer': 0
|
||||
},
|
||||
'useCache': True,
|
||||
'fields': ["game_id", "game_name", "comp_main", "comp_plus", "comp_100"] # Request only needed fields
|
||||
}
|
||||
if config.api_key:
|
||||
payload['searchOptions']['users']['id'] = config.api_key
|
||||
return payload
|
||||
|
||||
class ResultParser:
|
||||
"""Парсер результатов поиска."""
|
||||
def __init__(self, search_query: str, minimum_similarity: float = 0.4, case_sensitive: bool = True):
|
||||
self.search_query = search_query
|
||||
self.minimum_similarity = minimum_similarity
|
||||
self.case_sensitive = case_sensitive
|
||||
self.search_numbers = self._extract_numbers(search_query)
|
||||
|
||||
def parse_results(self, json_response: str, target_game_id: int | None = None) -> list[GameEntry]:
|
||||
try:
|
||||
data = orjson.loads(json_response)
|
||||
games = []
|
||||
# Only process the first result
|
||||
if data.get("data"):
|
||||
game_data = data["data"][0]
|
||||
game = self._parse_game_entry(game_data)
|
||||
if target_game_id is not None:
|
||||
if game.game_id == target_game_id:
|
||||
games.append(game)
|
||||
elif self.minimum_similarity == 0.0 or game.similarity >= self.minimum_similarity:
|
||||
games.append(game)
|
||||
return games
|
||||
except (orjson.JSONDecodeError, KeyError, IndexError):
|
||||
return []
|
||||
|
||||
def _parse_game_entry(self, game_data: dict[str, Any]) -> GameEntry:
|
||||
game = GameEntry()
|
||||
game.game_id = game_data.get("game_id", -1)
|
||||
game.game_name = game_data.get("game_name")
|
||||
game.raw_data = game_data
|
||||
time_fields = [
|
||||
("comp_main", "main_story"),
|
||||
("comp_plus", "main_extra"),
|
||||
("comp_100", "completionist")
|
||||
]
|
||||
all_zero = all(game_data.get(json_field, 0) == 0 for json_field, _ in time_fields)
|
||||
for json_field, attr_name in time_fields:
|
||||
if json_field in game_data:
|
||||
time_seconds = game_data[json_field]
|
||||
time_hours = None if all_zero else round(time_seconds / 3600, 2)
|
||||
setattr(game, attr_name, time_hours)
|
||||
game.similarity = self._calculate_similarity(game)
|
||||
return game
|
||||
|
||||
def _calculate_similarity(self, game: GameEntry) -> float:
|
||||
return self._compare_strings(self.search_query, game.game_name)
|
||||
|
||||
def _compare_strings(self, a: str | None, b: str | None) -> float:
|
||||
if not a or not b:
|
||||
return 0.0
|
||||
if self.case_sensitive:
|
||||
similarity = SequenceMatcher(None, a, b).ratio()
|
||||
else:
|
||||
similarity = SequenceMatcher(None, a.lower(), b.lower()).ratio()
|
||||
if self.search_numbers and not self._contains_numbers(b, self.search_numbers):
|
||||
similarity -= 0.1
|
||||
return max(0.0, similarity)
|
||||
|
||||
@staticmethod
|
||||
def _extract_numbers(text: str) -> list[str]:
|
||||
return [word for word in text.split() if word.isdigit()]
|
||||
|
||||
@staticmethod
|
||||
def _contains_numbers(text: str, numbers: list[str]) -> bool:
|
||||
if not numbers:
|
||||
return True
|
||||
cleaned_text = re.sub(r'([^\s\w]|_)+', '', text)
|
||||
text_numbers = [word for word in cleaned_text.split() if word.isdigit()]
|
||||
return any(num in text_numbers for num in numbers)
|
||||
|
||||
def get_cache_dir():
|
||||
"""Возвращает путь к каталогу кэша, создаёт его при необходимости."""
|
||||
xdg_cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache"))
|
||||
cache_dir = os.path.join(xdg_cache_home, "PortProtonQt")
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
return cache_dir
|
||||
|
||||
class HowLongToBeat(QObject):
|
||||
"""Основной класс для работы с API HowLongToBeat."""
|
||||
searchCompleted = Signal(list)
|
||||
|
||||
def __init__(self, minimum_similarity: float = 0.4, timeout: int = 60, parent=None):
|
||||
super().__init__(parent)
|
||||
self.minimum_similarity = minimum_similarity
|
||||
self.http_client = HTTPClient(timeout)
|
||||
self.cache_dir = get_cache_dir()
|
||||
|
||||
def _get_cache_file_path(self, game_name: str) -> str:
|
||||
"""Возвращает путь к файлу кэша для заданного имени игры."""
|
||||
safe_game_name = re.sub(r'[^\w\s-]', '', game_name).replace(' ', '_').lower()
|
||||
cache_file = f"hltb_{safe_game_name}.json"
|
||||
return os.path.join(self.cache_dir, cache_file)
|
||||
|
||||
def _load_from_cache(self, game_name: str) -> str | None:
|
||||
"""Пытается загрузить данные из кэша, если они существуют."""
|
||||
cache_file = self._get_cache_file_path(game_name)
|
||||
try:
|
||||
if os.path.exists(cache_file):
|
||||
with open(cache_file, 'rb') as f:
|
||||
return f.read().decode('utf-8')
|
||||
except (OSError, UnicodeDecodeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
def _save_to_cache(self, game_name: str, json_response: str):
|
||||
"""Сохраняет данные в кэш, храня только первую игру и необходимые поля."""
|
||||
cache_file = self._get_cache_file_path(game_name)
|
||||
try:
|
||||
# Парсим JSON и берем только первую игру
|
||||
data = orjson.loads(json_response)
|
||||
if data.get("data"):
|
||||
first_game = data["data"][0]
|
||||
simplified_data = {
|
||||
"data": [{
|
||||
"game_id": first_game.get("game_id", -1),
|
||||
"game_name": first_game.get("game_name"),
|
||||
"comp_main": first_game.get("comp_main", 0),
|
||||
"comp_plus": first_game.get("comp_plus", 0),
|
||||
"comp_100": first_game.get("comp_100", 0)
|
||||
}]
|
||||
}
|
||||
with open(cache_file, 'wb') as f:
|
||||
f.write(orjson.dumps(simplified_data))
|
||||
except (OSError, orjson.JSONDecodeError, IndexError):
|
||||
pass
|
||||
|
||||
def search(self, game_name: str, case_sensitive: bool = True) -> list[GameEntry] | None:
|
||||
if not game_name or not game_name.strip():
|
||||
return None
|
||||
# Проверяем кэш
|
||||
cached_response = self._load_from_cache(game_name)
|
||||
if cached_response:
|
||||
try:
|
||||
cached_data = orjson.loads(cached_response)
|
||||
full_json = {
|
||||
"data": [
|
||||
{
|
||||
"game_id": game["game_id"],
|
||||
"game_name": game["game_name"],
|
||||
"comp_main": game["comp_main"],
|
||||
"comp_plus": game["comp_plus"],
|
||||
"comp_100": game["comp_100"]
|
||||
}
|
||||
for game in cached_data.get("data", [])
|
||||
]
|
||||
}
|
||||
parser = ResultParser(
|
||||
game_name,
|
||||
self.minimum_similarity,
|
||||
case_sensitive
|
||||
)
|
||||
return parser.parse_results(orjson.dumps(full_json).decode('utf-8'))
|
||||
except orjson.JSONDecodeError:
|
||||
pass
|
||||
# Если нет в кэше, делаем запрос
|
||||
json_response = self.http_client.search_games(game_name)
|
||||
if not json_response:
|
||||
return None
|
||||
# Сохраняем в кэш только первую игру
|
||||
self._save_to_cache(game_name, json_response)
|
||||
parser = ResultParser(
|
||||
game_name,
|
||||
self.minimum_similarity,
|
||||
case_sensitive
|
||||
)
|
||||
return parser.parse_results(json_response)
|
||||
|
||||
def format_game_time(self, game_entry: GameEntry, time_field: str = "main_story") -> str | None:
|
||||
time_value = getattr(game_entry, time_field, None)
|
||||
if time_value is None:
|
||||
return None
|
||||
time_seconds = int(time_value * 3600)
|
||||
return format_playtime(time_seconds)
|
||||
|
||||
def search_with_callback(self, game_name: str, case_sensitive: bool = True):
|
||||
"""Выполняет поиск игры в фоновом потоке и испускает сигнал с результатами."""
|
||||
def search_thread():
|
||||
try:
|
||||
results = self.search(game_name, case_sensitive)
|
||||
self.searchCompleted.emit(results if results else [])
|
||||
except Exception as e:
|
||||
print(f"Error in search_with_callback: {e}")
|
||||
self.searchCompleted.emit([])
|
||||
|
||||
thread = Thread(target=search_thread)
|
||||
thread.daemon = True
|
||||
thread.start()
|
@@ -21,6 +21,13 @@ image_load_queue = Queue()
|
||||
image_executor = ThreadPoolExecutor(max_workers=4)
|
||||
queue_lock = threading.Lock()
|
||||
|
||||
def get_device_pixel_ratio() -> float:
|
||||
"""
|
||||
Retrieves the device pixel ratio from QApplication, with a fallback of 1.0 if not available.
|
||||
"""
|
||||
app = QApplication.instance()
|
||||
return app.devicePixelRatio() if isinstance(app, QApplication) else 1.0
|
||||
|
||||
def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[QPixmap], None], app_name: str = ""):
|
||||
"""
|
||||
Асинхронно загружает обложку через очередь задач.
|
||||
@@ -164,7 +171,6 @@ class FullscreenDialog(QDialog):
|
||||
:param theme: Объект темы для стилизации (если None, используется default_styles)
|
||||
"""
|
||||
super().__init__(parent)
|
||||
# Удаление диалога после закрытия
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
|
||||
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.setFocus()
|
||||
@@ -173,14 +179,12 @@ class FullscreenDialog(QDialog):
|
||||
self.current_index = current_index
|
||||
self.theme = theme if theme else default_styles
|
||||
|
||||
# Убираем стандартные элементы управления окна
|
||||
self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Dialog)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
|
||||
self.init_ui()
|
||||
self.update_display()
|
||||
|
||||
# Фильтруем события для закрытия диалога по клику
|
||||
self.imageLabel.installEventFilter(self)
|
||||
self.captionLabel.installEventFilter(self)
|
||||
|
||||
@@ -190,32 +194,28 @@ class FullscreenDialog(QDialog):
|
||||
self.mainLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.mainLayout.setSpacing(0)
|
||||
|
||||
# Контейнер для изображения и стрелок
|
||||
self.imageContainer = QWidget()
|
||||
self.imageContainer.setFixedSize(self.FIXED_WIDTH, self.FIXED_HEIGHT)
|
||||
self.imageContainerLayout = QHBoxLayout(self.imageContainer)
|
||||
self.imageContainerLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.imageContainerLayout.setSpacing(0)
|
||||
|
||||
# Левая стрелка
|
||||
self.prevButton = QToolButton()
|
||||
self.prevButton.setArrowType(Qt.ArrowType.LeftArrow)
|
||||
self.prevButton.setStyleSheet(self.theme.PREV_BUTTON_STYLE)
|
||||
self.prevButton.setStyleSheet(getattr(self.theme, "PREV_BUTTON_STYLE", ""))
|
||||
self.prevButton.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.prevButton.setFixedSize(40, 40)
|
||||
self.prevButton.clicked.connect(self.show_prev)
|
||||
self.imageContainerLayout.addWidget(self.prevButton)
|
||||
|
||||
# Метка для изображения
|
||||
self.imageLabel = QLabel()
|
||||
self.imageLabel.setFixedSize(self.FIXED_WIDTH - 80, self.FIXED_HEIGHT)
|
||||
self.imageLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.imageContainerLayout.addWidget(self.imageLabel, stretch=1)
|
||||
|
||||
# Правая стрелка
|
||||
self.nextButton = QToolButton()
|
||||
self.nextButton.setArrowType(Qt.ArrowType.RightArrow)
|
||||
self.nextButton.setStyleSheet(self.theme.NEXT_BUTTON_STYLE)
|
||||
self.nextButton.setStyleSheet(getattr(self.theme, "NEXT_BUTTON_STYLE", ""))
|
||||
self.nextButton.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.nextButton.setFixedSize(40, 40)
|
||||
self.nextButton.clicked.connect(self.show_next)
|
||||
@@ -223,16 +223,14 @@ class FullscreenDialog(QDialog):
|
||||
|
||||
self.mainLayout.addWidget(self.imageContainer)
|
||||
|
||||
# Небольшой отступ между изображением и подписью
|
||||
spacer = QSpacerItem(20, 10, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)
|
||||
self.mainLayout.addItem(spacer)
|
||||
|
||||
# Подпись
|
||||
self.captionLabel = QLabel()
|
||||
self.captionLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.captionLabel.setFixedHeight(40)
|
||||
self.captionLabel.setWordWrap(True)
|
||||
self.captionLabel.setStyleSheet(self.theme.CAPTION_LABEL_STYLE)
|
||||
self.captionLabel.setStyleSheet(getattr(self.theme, "CAPTION_LABEL_STYLE", ""))
|
||||
self.captionLabel.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.mainLayout.addWidget(self.captionLabel)
|
||||
|
||||
@@ -241,28 +239,37 @@ class FullscreenDialog(QDialog):
|
||||
if not self.images:
|
||||
return
|
||||
|
||||
# Очищаем старое содержимое
|
||||
self.imageLabel.clear()
|
||||
self.captionLabel.clear()
|
||||
QApplication.processEvents()
|
||||
|
||||
pixmap, caption = self.images[self.current_index]
|
||||
# Масштабируем изображение так, чтобы оно поместилось в область фиксированного размера
|
||||
# Учитываем devicePixelRatio для масштабирования высокого качества
|
||||
device_pixel_ratio = get_device_pixel_ratio()
|
||||
target_width = int((self.FIXED_WIDTH - 80) * device_pixel_ratio)
|
||||
target_height = int(self.FIXED_HEIGHT * device_pixel_ratio)
|
||||
|
||||
# Масштабируем изображение из оригинального pixmap
|
||||
scaled_pixmap = pixmap.scaled(
|
||||
self.FIXED_WIDTH - 80, # учитываем ширину стрелок
|
||||
self.FIXED_HEIGHT,
|
||||
target_width,
|
||||
target_height,
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation
|
||||
)
|
||||
scaled_pixmap.setDevicePixelRatio(device_pixel_ratio)
|
||||
self.imageLabel.setPixmap(scaled_pixmap)
|
||||
self.captionLabel.setText(caption)
|
||||
self.setWindowTitle(caption)
|
||||
|
||||
# Принудительная перерисовка виджетов
|
||||
self.imageLabel.repaint()
|
||||
self.captionLabel.repaint()
|
||||
self.repaint()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
"""Обновляет изображение при изменении размера окна."""
|
||||
super().resizeEvent(event)
|
||||
self.update_display() # Перерисовываем изображение с учетом нового размера
|
||||
|
||||
def show_prev(self):
|
||||
"""Показывает предыдущее изображение."""
|
||||
if self.images:
|
||||
@@ -292,7 +299,6 @@ class FullscreenDialog(QDialog):
|
||||
def mousePressEvent(self, event):
|
||||
"""Закрывает диалог при клике на пустую область."""
|
||||
pos = event.pos()
|
||||
# Проверяем, находится ли клик вне imageContainer и captionLabel
|
||||
if not (self.imageContainer.geometry().contains(pos) or
|
||||
self.captionLabel.geometry().contains(pos)):
|
||||
self.close()
|
||||
@@ -305,15 +311,14 @@ class ClickablePixmapItem(QGraphicsPixmapItem):
|
||||
"""
|
||||
def __init__(self, pixmap, caption="Просмотр изображения", images_list=None, index=0, carousel=None):
|
||||
"""
|
||||
:param pixmap: QPixmap для отображения в карусели
|
||||
:param pixmap: QPixmap для отображения в карусели (оригинальное, высокое разрешение)
|
||||
:param caption: Подпись к изображению
|
||||
:param images_list: Список всех изображений (кортежей (QPixmap, caption)),
|
||||
чтобы в диалоге можно было перелистывать.
|
||||
Если не передан, будет использован только текущее изображение.
|
||||
:param index: Индекс текущего изображения в images_list.
|
||||
:param carousel: Ссылка на родительскую карусель (ImageCarousel) для управления стрелками.
|
||||
:param images_list: Список всех изображений (кортежей (QPixmap, caption))
|
||||
:param index: Индекс текущего изображения в images_list
|
||||
:param carousel: Ссылка на родительскую карусель (ImageCarousel)
|
||||
"""
|
||||
super().__init__(pixmap)
|
||||
super().__init__()
|
||||
self.original_pixmap = pixmap # Store original high-resolution pixmap
|
||||
self.caption = caption
|
||||
self.images_list = images_list if images_list is not None else [(pixmap, caption)]
|
||||
self.index = index
|
||||
@@ -323,6 +328,20 @@ class ClickablePixmapItem(QGraphicsPixmapItem):
|
||||
self._click_start_position = None
|
||||
self.setAcceptedMouseButtons(Qt.MouseButton.LeftButton)
|
||||
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
|
||||
self.update_pixmap() # Set initial pixmap
|
||||
|
||||
def update_pixmap(self, height=300):
|
||||
"""Update the displayed pixmap by scaling from the original high-resolution pixmap."""
|
||||
if self.original_pixmap.isNull():
|
||||
return
|
||||
# Scale pixmap to desired height, considering device pixel ratio
|
||||
device_pixel_ratio = get_device_pixel_ratio()
|
||||
scaled_pixmap = self.original_pixmap.scaledToHeight(
|
||||
int(height * device_pixel_ratio),
|
||||
Qt.TransformationMode.SmoothTransformation
|
||||
)
|
||||
scaled_pixmap.setDevicePixelRatio(device_pixel_ratio)
|
||||
self.setPixmap(scaled_pixmap)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
@@ -339,17 +358,14 @@ class ClickablePixmapItem(QGraphicsPixmapItem):
|
||||
event.accept()
|
||||
|
||||
def show_fullscreen(self):
|
||||
# Скрываем стрелки карусели перед открытием FullscreenDialog
|
||||
if self.carousel:
|
||||
self.carousel.prevArrow.hide()
|
||||
self.carousel.nextArrow.hide()
|
||||
dialog = FullscreenDialog(self.images_list, current_index=self.index)
|
||||
dialog.exec()
|
||||
# После закрытия диалога обновляем видимость стрелок
|
||||
if self.carousel:
|
||||
self.carousel.update_arrows_visibility()
|
||||
|
||||
|
||||
class ImageCarousel(QGraphicsView):
|
||||
"""
|
||||
Карусель изображений с адаптивностью, возможностью увеличения по клику
|
||||
@@ -357,19 +373,16 @@ class ImageCarousel(QGraphicsView):
|
||||
"""
|
||||
def __init__(self, images: list[tuple], parent: QWidget | None = None, theme: object | None = None):
|
||||
super().__init__(parent)
|
||||
|
||||
# Аннотируем тип scene как QGraphicsScene
|
||||
self.carousel_scene: QGraphicsScene = QGraphicsScene(self)
|
||||
self.setScene(self.carousel_scene)
|
||||
|
||||
self.images = images # Список кортежей: (QPixmap, caption)
|
||||
self.image_items = []
|
||||
self._animation = None
|
||||
self.theme = theme if theme else default_styles
|
||||
self.max_height = 300 # Default height for images
|
||||
self.init_ui()
|
||||
self.create_arrows()
|
||||
|
||||
# Переменные для поддержки перетаскивания
|
||||
self._drag_active = False
|
||||
self._drag_start_position = None
|
||||
self._scroll_start_value = None
|
||||
@@ -380,30 +393,38 @@ class ImageCarousel(QGraphicsView):
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self.setFrameShape(QFrame.Shape.NoFrame)
|
||||
|
||||
x_offset = 10 # Отступ между изображениями
|
||||
max_height = 300 # Фиксированная высота изображений
|
||||
self.update_scene()
|
||||
|
||||
def update_scene(self):
|
||||
"""Update the scene with scaled images based on current size and scale."""
|
||||
self.carousel_scene.clear()
|
||||
self.image_items.clear()
|
||||
|
||||
x_offset = 10
|
||||
x = 0
|
||||
device_pixel_ratio = get_device_pixel_ratio()
|
||||
|
||||
for i, (pixmap, caption) in enumerate(self.images):
|
||||
item = ClickablePixmapItem(
|
||||
pixmap.scaledToHeight(max_height, Qt.TransformationMode.SmoothTransformation),
|
||||
pixmap, # Pass original pixmap
|
||||
caption,
|
||||
images_list=self.images,
|
||||
index=i,
|
||||
carousel=self # Передаем ссылку на карусель
|
||||
carousel=self
|
||||
)
|
||||
item.update_pixmap(self.max_height) # Scale to current height
|
||||
item.setPos(x, 0)
|
||||
self.carousel_scene.addItem(item)
|
||||
self.image_items.append(item)
|
||||
x += item.pixmap().width() + x_offset
|
||||
x += item.pixmap().width() / device_pixel_ratio + x_offset
|
||||
|
||||
self.setSceneRect(0, 0, x, max_height)
|
||||
self.setSceneRect(0, 0, x, self.max_height)
|
||||
|
||||
def create_arrows(self):
|
||||
"""Создаёт кнопки-стрелки и привязывает их к функциям прокрутки."""
|
||||
self.prevArrow = QToolButton(self)
|
||||
self.prevArrow.setArrowType(Qt.ArrowType.LeftArrow)
|
||||
self.prevArrow.setStyleSheet(self.theme.PREV_BUTTON_STYLE) # type: ignore
|
||||
self.prevArrow.setStyleSheet(getattr(self.theme, "PREV_BUTTON_STYLE", ""))
|
||||
self.prevArrow.setFixedSize(40, 40)
|
||||
self.prevArrow.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.prevArrow.setAutoRepeat(True)
|
||||
@@ -414,7 +435,7 @@ class ImageCarousel(QGraphicsView):
|
||||
|
||||
self.nextArrow = QToolButton(self)
|
||||
self.nextArrow.setArrowType(Qt.ArrowType.RightArrow)
|
||||
self.nextArrow.setStyleSheet(self.theme.NEXT_BUTTON_STYLE) # type: ignore
|
||||
self.nextArrow.setStyleSheet(getattr(self.theme, "NEXT_BUTTON_STYLE", ""))
|
||||
self.nextArrow.setFixedSize(40, 40)
|
||||
self.nextArrow.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.nextArrow.setAutoRepeat(True)
|
||||
@@ -423,14 +444,9 @@ class ImageCarousel(QGraphicsView):
|
||||
self.nextArrow.clicked.connect(self.scroll_right)
|
||||
self.nextArrow.raise_()
|
||||
|
||||
# Проверяем видимость стрелок при создании
|
||||
self.update_arrows_visibility()
|
||||
|
||||
def update_arrows_visibility(self):
|
||||
"""
|
||||
Показывает стрелки, если контент шире видимой области.
|
||||
Иначе скрывает их.
|
||||
"""
|
||||
if hasattr(self, "prevArrow") and hasattr(self, "nextArrow"):
|
||||
if self.horizontalScrollBar().maximum() == 0:
|
||||
self.prevArrow.hide()
|
||||
@@ -444,7 +460,8 @@ class ImageCarousel(QGraphicsView):
|
||||
margin = 10
|
||||
self.prevArrow.move(margin, (self.height() - self.prevArrow.height()) // 2)
|
||||
self.nextArrow.move(self.width() - self.nextArrow.width() - margin,
|
||||
(self.height() - self.nextArrow.height()) // 2)
|
||||
(self.height() - self.nextArrow.height()) // 2)
|
||||
self.update_scene() # Re-scale images on resize
|
||||
self.update_arrows_visibility()
|
||||
|
||||
def animate_scroll(self, end_value):
|
||||
@@ -469,19 +486,15 @@ class ImageCarousel(QGraphicsView):
|
||||
self.animate_scroll(new_value)
|
||||
|
||||
def update_images(self, new_images):
|
||||
self.carousel_scene.clear()
|
||||
self.images = new_images
|
||||
self.image_items.clear()
|
||||
self.init_ui()
|
||||
self.update_scene()
|
||||
self.update_arrows_visibility()
|
||||
|
||||
# Обработка событий мыши для перетаскивания
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self._drag_active = True
|
||||
self._drag_start_position = event.pos()
|
||||
self._scroll_start_value = self.horizontalScrollBar().value()
|
||||
# Скрываем стрелки при начале перетаскивания
|
||||
if hasattr(self, "prevArrow"):
|
||||
self.prevArrow.hide()
|
||||
if hasattr(self, "nextArrow"):
|
||||
@@ -497,6 +510,5 @@ class ImageCarousel(QGraphicsView):
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
self._drag_active = False
|
||||
# Показываем стрелки после завершения перетаскивания (с проверкой видимости)
|
||||
self.update_arrows_visibility()
|
||||
super().mouseReleaseEvent(event)
|
||||
|
@@ -111,6 +111,8 @@ class InputManager(QObject):
|
||||
self.stick_value = 0 # Текущее значение стика (для плавности)
|
||||
self.dead_zone = 8000 # Мертвая зона стика
|
||||
|
||||
self._is_gamescope_session = 'gamescope' in os.environ.get('DESKTOP_SESSION', '').lower()
|
||||
|
||||
# Add variables for continuous D-pad movement
|
||||
self.dpad_timer = QTimer(self)
|
||||
self.dpad_timer.timeout.connect(self.handle_dpad_repeat)
|
||||
@@ -849,7 +851,7 @@ class InputManager(QObject):
|
||||
return True
|
||||
|
||||
# Toggle fullscreen with F11
|
||||
if key == Qt.Key.Key_F11:
|
||||
if key == Qt.Key.Key_F11 and not self._is_gamescope_session:
|
||||
self.toggle_fullscreen.emit(not self._is_fullscreen)
|
||||
return True
|
||||
|
||||
@@ -946,7 +948,7 @@ class InputManager(QObject):
|
||||
continue
|
||||
now = time.time()
|
||||
if event.type == ecodes.EV_KEY and event.value == 1:
|
||||
if event.code in BUTTONS['menu']:
|
||||
if event.code in BUTTONS['menu'] and not self._is_gamescope_session:
|
||||
self.toggle_fullscreen.emit(not self._is_fullscreen)
|
||||
else:
|
||||
self.button_pressed.emit(event.code)
|
||||
|
@@ -9,7 +9,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-07-06 17:56+0500\n"
|
||||
"POT-Creation-Date: 2025-07-14 13:16+0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: de_DE\n"
|
||||
@@ -563,6 +563,15 @@ msgstr ""
|
||||
msgid "PLAY TIME"
|
||||
msgstr ""
|
||||
|
||||
msgid "MAIN STORY"
|
||||
msgstr ""
|
||||
|
||||
msgid "MAIN + SIDES"
|
||||
msgstr ""
|
||||
|
||||
msgid "COMPLETIONIST"
|
||||
msgstr ""
|
||||
|
||||
msgid "full"
|
||||
msgstr ""
|
||||
|
||||
|
@@ -9,7 +9,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-07-06 17:56+0500\n"
|
||||
"POT-Creation-Date: 2025-07-14 13:16+0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: es_ES\n"
|
||||
@@ -563,6 +563,15 @@ msgstr ""
|
||||
msgid "PLAY TIME"
|
||||
msgstr ""
|
||||
|
||||
msgid "MAIN STORY"
|
||||
msgstr ""
|
||||
|
||||
msgid "MAIN + SIDES"
|
||||
msgstr ""
|
||||
|
||||
msgid "COMPLETIONIST"
|
||||
msgstr ""
|
||||
|
||||
msgid "full"
|
||||
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-07-06 17:56+0500\n"
|
||||
"POT-Creation-Date: 2025-07-14 13:16+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"
|
||||
@@ -561,6 +561,15 @@ msgstr ""
|
||||
msgid "PLAY TIME"
|
||||
msgstr ""
|
||||
|
||||
msgid "MAIN STORY"
|
||||
msgstr ""
|
||||
|
||||
msgid "MAIN + SIDES"
|
||||
msgstr ""
|
||||
|
||||
msgid "COMPLETIONIST"
|
||||
msgstr ""
|
||||
|
||||
msgid "full"
|
||||
msgstr ""
|
||||
|
||||
|
@@ -9,8 +9,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-07-06 17:56+0500\n"
|
||||
"PO-Revision-Date: 2025-07-06 17:56+0500\n"
|
||||
"POT-Creation-Date: 2025-07-14 13:16+0500\n"
|
||||
"PO-Revision-Date: 2025-07-14 13:16+0500\n"
|
||||
"Last-Translator: \n"
|
||||
"Language: ru_RU\n"
|
||||
"Language-Team: ru_RU <LL@li.org>\n"
|
||||
@@ -572,6 +572,15 @@ msgstr "Последний запуск"
|
||||
msgid "PLAY TIME"
|
||||
msgstr "Время игры"
|
||||
|
||||
msgid "MAIN STORY"
|
||||
msgstr "СЮЖЕТ"
|
||||
|
||||
msgid "MAIN + SIDES"
|
||||
msgstr "СЮЖЕТ + ПОБОЧКИ"
|
||||
|
||||
msgid "COMPLETIONIST"
|
||||
msgstr "100%"
|
||||
|
||||
msgid "full"
|
||||
msgstr "полная"
|
||||
|
||||
|
@@ -10,6 +10,7 @@ import psutil
|
||||
|
||||
from portprotonqt.dialogs import AddGameDialog, FileExplorer
|
||||
from portprotonqt.game_card import GameCard
|
||||
from portprotonqt.animations import DetailPageAnimations
|
||||
from portprotonqt.custom_widgets import FlowLayout, ClickableLabel, AutoSizeButton, NavLabel
|
||||
from portprotonqt.portproton_api import PortProtonAPI
|
||||
from portprotonqt.input_manager import InputManager
|
||||
@@ -31,23 +32,22 @@ from portprotonqt.config_utils import (
|
||||
)
|
||||
from portprotonqt.localization import _, get_egs_language, read_metadata_translations
|
||||
from portprotonqt.logger import get_logger
|
||||
from portprotonqt.howlongtobeat_api import HowLongToBeat
|
||||
from portprotonqt.downloader import Downloader
|
||||
|
||||
from PySide6.QtWidgets import (QLineEdit, QMainWindow, QStatusBar, QWidget, QVBoxLayout, QLabel, QHBoxLayout, QStackedWidget, QComboBox, QScrollArea, QSlider,
|
||||
QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QGraphicsEffect, QGraphicsOpacityEffect, QApplication, QPushButton, QProgressBar, QCheckBox)
|
||||
QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy)
|
||||
from PySide6.QtCore import Qt, QAbstractAnimation, QUrl, Signal, QTimer, Slot
|
||||
from PySide6.QtGui import QIcon, QPixmap, QColor, QDesktopServices
|
||||
from PySide6.QtCore import Qt, QAbstractAnimation, QPropertyAnimation, QByteArray, QUrl, Signal, QTimer, Slot
|
||||
from typing import cast
|
||||
from collections.abc import Callable
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime
|
||||
from PySide6.QtWidgets import QSizePolicy
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
"""Main window of PortProtonQt."""
|
||||
settings_saved = Signal()
|
||||
games_loaded = Signal(list)
|
||||
update_progress = Signal(int) # Signal to update progress bar
|
||||
update_status_message = Signal(str, int) # Signal to update status message
|
||||
@@ -209,6 +209,7 @@ class MainWindow(QMainWindow):
|
||||
self.restore_state()
|
||||
|
||||
self.input_manager = InputManager(self)
|
||||
self.detail_animations = DetailPageAnimations(self, self.theme)
|
||||
QTimer.singleShot(0, self.loadGames)
|
||||
|
||||
if read_fullscreen_config():
|
||||
@@ -697,6 +698,15 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super().resizeEvent(event)
|
||||
if hasattr(self, '_animations') and self._animations:
|
||||
for widget, animation in list(self._animations.items()):
|
||||
try:
|
||||
if animation.state() == QAbstractAnimation.State.Running:
|
||||
animation.stop()
|
||||
widget.setWindowOpacity(1.0)
|
||||
del self._animations[widget]
|
||||
except RuntimeError:
|
||||
del self._animations[widget]
|
||||
if not hasattr(self, '_last_width'):
|
||||
self._last_width = self.width()
|
||||
if abs(self.width() - self._last_width) > 10:
|
||||
@@ -1320,7 +1330,6 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self.settingsDebounceTimer.start()
|
||||
|
||||
self.settings_saved.emit()
|
||||
|
||||
# Управление полноэкранным режимом
|
||||
gamepad_connected = self.input_manager.find_gamepad() is not None
|
||||
@@ -1517,24 +1526,47 @@ class MainWindow(QMainWindow):
|
||||
self._animations = {}
|
||||
imageLabel = QLabel()
|
||||
imageLabel.setFixedSize(300, 400)
|
||||
self._detail_page_active = True
|
||||
self._current_detail_page = detailPage
|
||||
|
||||
if cover_path:
|
||||
def on_pixmap_ready(pixmap):
|
||||
rounded = round_corners(pixmap, 10)
|
||||
imageLabel.setPixmap(rounded)
|
||||
# Функция загрузки изображения и обновления стилей
|
||||
def load_image_and_restore_effect():
|
||||
if not detailPage or detailPage.isHidden():
|
||||
logger.warning("Detail page is None or hidden, skipping image load")
|
||||
return
|
||||
|
||||
def on_palette_ready(palette):
|
||||
dark_palette = [self.darkenColor(color, factor=200) for color in palette]
|
||||
stops = ",\n".join(
|
||||
[f"stop:{i/(len(dark_palette)-1):.2f} {dark_palette[i].name()}" for i in range(len(dark_palette))]
|
||||
)
|
||||
detailPage.setStyleSheet(self.theme.detail_page_style(stops))
|
||||
detailPage.setWindowOpacity(1.0)
|
||||
|
||||
self.getColorPalette_async(cover_path, num_colors=5, callback=on_palette_ready)
|
||||
if cover_path:
|
||||
def on_pixmap_ready(pixmap):
|
||||
if not detailPage or detailPage.isHidden():
|
||||
logger.warning("Detail page is None or hidden, skipping pixmap update")
|
||||
return
|
||||
rounded = round_corners(pixmap, 10)
|
||||
imageLabel.setPixmap(rounded)
|
||||
logger.debug("Pixmap set for imageLabel")
|
||||
|
||||
load_pixmap_async(cover_path, 300, 400, on_pixmap_ready)
|
||||
else:
|
||||
detailPage.setStyleSheet(self.theme.DETAIL_PAGE_NO_COVER_STYLE)
|
||||
def on_palette_ready(palette):
|
||||
if not detailPage or detailPage.isHidden():
|
||||
logger.warning("Detail page is None or hidden, skipping palette update")
|
||||
return
|
||||
dark_palette = [self.darkenColor(color, factor=200) for color in palette]
|
||||
stops = ",\n".join(
|
||||
[f"stop:{i/(len(dark_palette)-1):.2f} {dark_palette[i].name()}" for i in range(len(dark_palette))]
|
||||
)
|
||||
detailPage.setStyleSheet(self.theme.detail_page_style(stops))
|
||||
detailPage.update()
|
||||
logger.debug("Stylesheet updated with palette")
|
||||
|
||||
self.getColorPalette_async(cover_path, num_colors=5, callback=on_palette_ready)
|
||||
load_pixmap_async(cover_path, 300, 400, on_pixmap_ready)
|
||||
else:
|
||||
detailPage.setStyleSheet(self.theme.DETAIL_PAGE_NO_COVER_STYLE)
|
||||
detailPage.update()
|
||||
|
||||
def cleanup_animation():
|
||||
if detailPage in self._animations:
|
||||
del self._animations[detailPage]
|
||||
|
||||
mainLayout = QVBoxLayout(detailPage)
|
||||
mainLayout.setContentsMargins(30, 30, 30, 30)
|
||||
@@ -1589,7 +1621,7 @@ class MainWindow(QMainWindow):
|
||||
badge_spacing = 5
|
||||
top_y = 10
|
||||
badge_y_positions = []
|
||||
badge_width = int(300 * 2/3) # 2/3 ширины обложки (300 px)
|
||||
badge_width = int(300 * 2/3)
|
||||
|
||||
# ProtonDB бейдж
|
||||
protondb_text = GameCard.getProtonDBText(protondb_tier)
|
||||
@@ -1642,7 +1674,7 @@ class MainWindow(QMainWindow):
|
||||
egsLabel.setVisible(egs_visible)
|
||||
|
||||
# PortProton badge
|
||||
portproton_icon = self.theme_manager.get_icon("ppqt-tray")
|
||||
portproton_icon = self.theme_manager.get_icon("portproton")
|
||||
portprotonLabel = ClickableLabel(
|
||||
"PortProton",
|
||||
icon=portproton_icon,
|
||||
@@ -1678,11 +1710,6 @@ class MainWindow(QMainWindow):
|
||||
anticheat_visible = False
|
||||
|
||||
# Расположение бейджей
|
||||
right_margin = 8
|
||||
badge_spacing = 5
|
||||
top_y = 10
|
||||
badge_y_positions = []
|
||||
badge_width = int(300 * 2/3)
|
||||
if steam_visible:
|
||||
steam_x = 300 - badge_width - right_margin
|
||||
steamLabel.move(steam_x, top_y)
|
||||
@@ -1736,22 +1763,102 @@ class MainWindow(QMainWindow):
|
||||
descLabel.setStyleSheet(self.theme.DETAIL_PAGE_DESC_STYLE)
|
||||
detailsLayout.addWidget(descLabel)
|
||||
|
||||
infoLayout = QHBoxLayout()
|
||||
infoLayout.setSpacing(10)
|
||||
# Инициализация HowLongToBeat
|
||||
hltb = HowLongToBeat(parent=self)
|
||||
|
||||
# Создаем общий layout для всей игровой информации
|
||||
gameInfoLayout = QVBoxLayout()
|
||||
gameInfoLayout.setSpacing(10)
|
||||
|
||||
# Первая строка: Last Launch и Play Time
|
||||
firstRowLayout = QHBoxLayout()
|
||||
firstRowLayout.setSpacing(10)
|
||||
|
||||
# Last Launch
|
||||
lastLaunchTitle = QLabel(_("LAST LAUNCH"))
|
||||
lastLaunchTitle.setStyleSheet(self.theme.LAST_LAUNCH_TITLE_STYLE)
|
||||
lastLaunchValue = QLabel(last_launch)
|
||||
lastLaunchValue.setStyleSheet(self.theme.LAST_LAUNCH_VALUE_STYLE)
|
||||
firstRowLayout.addWidget(lastLaunchTitle)
|
||||
firstRowLayout.addWidget(lastLaunchValue)
|
||||
firstRowLayout.addSpacing(30)
|
||||
|
||||
# Play Time
|
||||
playTimeTitle = QLabel(_("PLAY TIME"))
|
||||
playTimeTitle.setStyleSheet(self.theme.PLAY_TIME_TITLE_STYLE)
|
||||
playTimeValue = QLabel(formatted_playtime)
|
||||
playTimeValue.setStyleSheet(self.theme.PLAY_TIME_VALUE_STYLE)
|
||||
infoLayout.addWidget(lastLaunchTitle)
|
||||
infoLayout.addWidget(lastLaunchValue)
|
||||
infoLayout.addSpacing(30)
|
||||
infoLayout.addWidget(playTimeTitle)
|
||||
infoLayout.addWidget(playTimeValue)
|
||||
detailsLayout.addLayout(infoLayout)
|
||||
firstRowLayout.addWidget(playTimeTitle)
|
||||
firstRowLayout.addWidget(playTimeValue)
|
||||
|
||||
gameInfoLayout.addLayout(firstRowLayout)
|
||||
|
||||
# Создаем placeholder для второй строки (HLTB данные)
|
||||
hltbLayout = QHBoxLayout()
|
||||
hltbLayout.setSpacing(10)
|
||||
|
||||
# Время прохождения (Main Story, Main + Sides, Completionist)
|
||||
def on_hltb_results(results):
|
||||
if not hasattr(self, '_detail_page_active') or not self._detail_page_active:
|
||||
return
|
||||
if not self._current_detail_page or self._current_detail_page.isHidden() or not self._current_detail_page.parent():
|
||||
return
|
||||
|
||||
if results:
|
||||
game = results[0] # Берем первый результат
|
||||
main_story_time = hltb.format_game_time(game, "main_story")
|
||||
main_extra_time = hltb.format_game_time(game, "main_extra")
|
||||
completionist_time = hltb.format_game_time(game, "completionist")
|
||||
|
||||
# Очищаем layout перед добавлением новых элементов
|
||||
while hltbLayout.count():
|
||||
child = hltbLayout.takeAt(0)
|
||||
if child.widget():
|
||||
child.widget().deleteLater()
|
||||
|
||||
has_data = False
|
||||
|
||||
if main_story_time is not None:
|
||||
mainStoryTitle = QLabel(_("MAIN STORY"))
|
||||
mainStoryTitle.setStyleSheet(self.theme.LAST_LAUNCH_TITLE_STYLE)
|
||||
mainStoryValue = QLabel(main_story_time)
|
||||
mainStoryValue.setStyleSheet(self.theme.LAST_LAUNCH_VALUE_STYLE)
|
||||
hltbLayout.addWidget(mainStoryTitle)
|
||||
hltbLayout.addWidget(mainStoryValue)
|
||||
hltbLayout.addSpacing(30)
|
||||
has_data = True
|
||||
|
||||
if main_extra_time is not None:
|
||||
mainExtraTitle = QLabel(_("MAIN + SIDES"))
|
||||
mainExtraTitle.setStyleSheet(self.theme.PLAY_TIME_TITLE_STYLE)
|
||||
mainExtraValue = QLabel(main_extra_time)
|
||||
mainExtraValue.setStyleSheet(self.theme.PLAY_TIME_VALUE_STYLE)
|
||||
hltbLayout.addWidget(mainExtraTitle)
|
||||
hltbLayout.addWidget(mainExtraValue)
|
||||
hltbLayout.addSpacing(30)
|
||||
has_data = True
|
||||
|
||||
if completionist_time is not None:
|
||||
completionistTitle = QLabel(_("COMPLETIONIST"))
|
||||
completionistTitle.setStyleSheet(self.theme.LAST_LAUNCH_TITLE_STYLE)
|
||||
completionistValue = QLabel(completionist_time)
|
||||
completionistValue.setStyleSheet(self.theme.LAST_LAUNCH_VALUE_STYLE)
|
||||
hltbLayout.addWidget(completionistTitle)
|
||||
hltbLayout.addWidget(completionistValue)
|
||||
has_data = True
|
||||
|
||||
# Если есть данные, добавляем layout во вторую строку
|
||||
if has_data:
|
||||
gameInfoLayout.addLayout(hltbLayout)
|
||||
|
||||
# Подключаем сигнал searchCompleted к on_hltb_results
|
||||
hltb.searchCompleted.connect(on_hltb_results)
|
||||
|
||||
# Запускаем поиск в фоновом потоке
|
||||
hltb.search_with_callback(name, case_sensitive=False)
|
||||
|
||||
# Добавляем общий layout с игровой информацией
|
||||
detailsLayout.addLayout(gameInfoLayout)
|
||||
|
||||
if controller_support:
|
||||
cs = controller_support.lower()
|
||||
@@ -1769,7 +1876,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
detailsLayout.addStretch(1)
|
||||
|
||||
# Определяем текущий идентификатор игры по exec_line для корректного отображения кнопки
|
||||
# Определяем текущий идентификатор игры по exec_line
|
||||
entry_exec_split = shlex.split(exec_line)
|
||||
if not entry_exec_split:
|
||||
return
|
||||
@@ -1802,17 +1909,7 @@ class MainWindow(QMainWindow):
|
||||
self.current_play_button = playButton
|
||||
|
||||
# Анимация
|
||||
opacityEffect = QGraphicsOpacityEffect(detailPage)
|
||||
detailPage.setGraphicsEffect(opacityEffect)
|
||||
animation = QPropertyAnimation(opacityEffect, QByteArray(b"opacity"))
|
||||
animation.setDuration(800)
|
||||
animation.setStartValue(0)
|
||||
animation.setEndValue(1)
|
||||
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||
self._animations[detailPage] = animation
|
||||
animation.finished.connect(
|
||||
lambda: detailPage.setGraphicsEffect(cast(QGraphicsEffect, None))
|
||||
)
|
||||
self.detail_animations.animate_detail_page(detailPage, load_image_and_restore_effect, cleanup_animation)
|
||||
|
||||
def toggleFavoriteInDetailPage(self, game_name, label):
|
||||
favorites = read_favorites()
|
||||
@@ -1868,14 +1965,42 @@ class MainWindow(QMainWindow):
|
||||
parent = parent.parent()
|
||||
|
||||
def goBackDetailPage(self, page: QWidget | None) -> None:
|
||||
if page is None or page != self.stackedWidget.currentWidget():
|
||||
if page is None or page != self.stackedWidget.currentWidget() or getattr(self, '_exit_animation_in_progress', False):
|
||||
return
|
||||
self.stackedWidget.setCurrentIndex(0)
|
||||
self.stackedWidget.removeWidget(page)
|
||||
page.deleteLater()
|
||||
self.currentDetailPage = None
|
||||
self.current_exec_line = None
|
||||
self.current_play_button = None
|
||||
self._exit_animation_in_progress = True
|
||||
self._detail_page_active = False
|
||||
self._current_detail_page = None
|
||||
|
||||
def cleanup():
|
||||
"""Helper function to clean up after animation."""
|
||||
try:
|
||||
if page in self._animations:
|
||||
animation = self._animations[page]
|
||||
try:
|
||||
if animation.state() == QAbstractAnimation.State.Running:
|
||||
animation.stop()
|
||||
except RuntimeError:
|
||||
pass # Animation already deleted
|
||||
finally:
|
||||
del self._animations[page]
|
||||
self.stackedWidget.setCurrentIndex(0)
|
||||
self.stackedWidget.removeWidget(page)
|
||||
page.deleteLater()
|
||||
self.currentDetailPage = None
|
||||
self.current_exec_line = None
|
||||
self.current_play_button = None
|
||||
self._exit_animation_in_progress = False
|
||||
except Exception as e:
|
||||
logger.error(f"Error in cleanup: {e}", exc_info=True)
|
||||
self._exit_animation_in_progress = False
|
||||
|
||||
# Start exit animation
|
||||
try:
|
||||
self.detail_animations.animate_detail_page_exit(page, cleanup)
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting exit animation: {e}", exc_info=True)
|
||||
self._exit_animation_in_progress = False
|
||||
cleanup() # Fallback to cleanup if animation fails
|
||||
|
||||
def is_target_exe_running(self):
|
||||
"""Проверяет, запущен ли процесс с именем self.target_exe через psutil."""
|
||||
|
@@ -18,6 +18,10 @@ from collections.abc import Callable
|
||||
import re
|
||||
import shutil
|
||||
import zlib
|
||||
import websocket
|
||||
import requests
|
||||
import random
|
||||
import base64
|
||||
|
||||
downloader = Downloader()
|
||||
logger = get_logger(__name__)
|
||||
@@ -291,7 +295,7 @@ def load_steam_apps_async(callback: Callable[[list], None]):
|
||||
if os.path.exists(cache_tar):
|
||||
os.remove(cache_tar)
|
||||
logger.info("Archive %s deleted after extraction", cache_tar)
|
||||
steam_apps = data.get("applist", {}).get("apps", []) if isinstance(data, dict) else data or []
|
||||
steam_apps = data if isinstance(data, list) else []
|
||||
logger.info("Loaded %d apps from archive", len(steam_apps))
|
||||
callback(steam_apps)
|
||||
except Exception as e:
|
||||
@@ -303,12 +307,25 @@ def load_steam_apps_async(callback: Callable[[list], None]):
|
||||
try:
|
||||
with open(cache_json, "rb") as f:
|
||||
data = orjson.loads(f.read())
|
||||
steam_apps = data.get("applist", {}).get("apps", []) if isinstance(data, dict) else data or []
|
||||
# Validate JSON structure
|
||||
if not isinstance(data, list):
|
||||
logger.error("Cached JSON %s has invalid format (not a list), re-downloading", cache_json)
|
||||
raise ValueError("Invalid JSON structure")
|
||||
# Validate each app entry
|
||||
for app in data:
|
||||
if not isinstance(app, dict) or "appid" not in app or "normalized_name" not in app:
|
||||
logger.error("Cached JSON %s contains invalid app entry, re-downloading", cache_json)
|
||||
raise ValueError("Invalid app entry structure")
|
||||
steam_apps = data
|
||||
logger.info("Loaded %d apps from cache", len(steam_apps))
|
||||
callback(steam_apps)
|
||||
except Exception as e:
|
||||
logger.error("Error reading cached JSON: %s", e)
|
||||
callback([])
|
||||
logger.error("Error reading or validating cached JSON %s: %s", cache_json, e)
|
||||
# Attempt to re-download if cache is invalid or corrupted
|
||||
app_list_url = (
|
||||
"https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/data/games_appid.tar.xz"
|
||||
)
|
||||
downloader.download_async(app_list_url, cache_tar, timeout=5, callback=process_tar)
|
||||
else:
|
||||
app_list_url = (
|
||||
"https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/data/games_appid.tar.xz"
|
||||
@@ -448,12 +465,25 @@ def load_weanticheatyet_data_async(callback: Callable[[list], None]):
|
||||
try:
|
||||
with open(cache_json, "rb") as f:
|
||||
data = orjson.loads(f.read())
|
||||
anti_cheat_data = data or []
|
||||
# Validate JSON structure
|
||||
if not isinstance(data, list):
|
||||
logger.error("Cached JSON %s has invalid format (not a list), re-downloading", cache_json)
|
||||
raise ValueError("Invalid JSON structure")
|
||||
# Validate each anti-cheat entry
|
||||
for entry in data:
|
||||
if not isinstance(entry, dict) or "normalized_name" not in entry or "status" not in entry:
|
||||
logger.error("Cached JSON %s contains invalid anti-cheat entry, re-downloading", cache_json)
|
||||
raise ValueError("Invalid anti-cheat entry structure")
|
||||
anti_cheat_data = data
|
||||
logger.info("Loaded %d anti-cheat entries from cache", len(anti_cheat_data))
|
||||
callback(anti_cheat_data)
|
||||
except Exception as e:
|
||||
logger.error("Error reading cached WeAntiCheatYet JSON: %s", e)
|
||||
callback([])
|
||||
logger.error("Error reading or validating cached WeAntiCheatYet JSON %s: %s", cache_json, e)
|
||||
# Attempt to re-download if cache is invalid or corrupted
|
||||
app_list_url = (
|
||||
"https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/data/anticheat_games.tar.xz"
|
||||
)
|
||||
downloader.download_async(app_list_url, cache_tar, timeout=5, callback=process_tar)
|
||||
else:
|
||||
app_list_url = (
|
||||
"https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/data/anticheat_games.tar.xz"
|
||||
@@ -745,6 +775,126 @@ def get_steam_apps_and_index_async(callback: Callable[[tuple[list, dict]], None]
|
||||
|
||||
load_steam_apps_async(on_steam_apps)
|
||||
|
||||
def enable_steam_cef() -> tuple[bool, str]:
|
||||
"""
|
||||
Проверяет и при необходимости активирует режим удаленной отладки Steam CEF.
|
||||
|
||||
Создает файл .cef-enable-remote-debugging в директории Steam.
|
||||
Steam необходимо перезапустить после первого создания этого файла.
|
||||
|
||||
Возвращает кортеж:
|
||||
- (True, "already_enabled") если уже было активно.
|
||||
- (True, "restart_needed") если было только что активировано и нужен перезапуск Steam.
|
||||
- (False, "steam_not_found") если директория Steam не найдена.
|
||||
"""
|
||||
steam_home = get_steam_home()
|
||||
if not steam_home:
|
||||
return (False, "steam_not_found")
|
||||
|
||||
cef_flag_file = steam_home / ".cef-enable-remote-debugging"
|
||||
logger.info(f"Проверка CEF флага: {cef_flag_file}")
|
||||
|
||||
if cef_flag_file.exists():
|
||||
logger.info("CEF Remote Debugging уже активирован.")
|
||||
return (True, "already_enabled")
|
||||
else:
|
||||
try:
|
||||
os.makedirs(cef_flag_file.parent, exist_ok=True)
|
||||
cef_flag_file.touch()
|
||||
logger.info("CEF Remote Debugging активирован. Steam необходимо перезапустить.")
|
||||
return (True, "restart_needed")
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось создать CEF флаг {cef_flag_file}: {e}")
|
||||
return (False, str(e))
|
||||
|
||||
def call_steam_api(js_cmd: str, *args) -> dict | None:
|
||||
"""
|
||||
Выполняет JavaScript функцию в контексте Steam через CEF Remote Debugging.
|
||||
|
||||
Args:
|
||||
js_cmd: Имя JS функции для вызова (напр. 'createShortcut').
|
||||
*args: Аргументы для передачи в JS функцию.
|
||||
|
||||
Returns:
|
||||
Словарь с результатом выполнения или None в случае ошибки.
|
||||
"""
|
||||
status, message = enable_steam_cef()
|
||||
if not (status is True and message == "already_enabled"):
|
||||
if message == "restart_needed":
|
||||
logger.warning("Steam CEF API доступен, но требует перезапуска Steam для полной активации.")
|
||||
elif message == "steam_not_found":
|
||||
logger.error("Не удалось найти директорию Steam для проверки CEF API.")
|
||||
else:
|
||||
logger.error(f"Steam CEF API недоступен или не готов: {message}")
|
||||
return None
|
||||
|
||||
steam_debug_url = "http://localhost:8080/json"
|
||||
|
||||
try:
|
||||
response = requests.get(steam_debug_url, timeout=2)
|
||||
response.raise_for_status()
|
||||
contexts = response.json()
|
||||
ws_url = next((ctx['webSocketDebuggerUrl'] for ctx in contexts if ctx['title'] == 'SharedJSContext'), None)
|
||||
if not ws_url:
|
||||
logger.warning("Не удалось найти SharedJSContext. Steam запущен с флагом -cef-enable-remote-debugging?")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось подключиться к Steam CEF API по адресу {steam_debug_url}. Steam запущен? {e}")
|
||||
return None
|
||||
|
||||
js_code = """
|
||||
async function createShortcut(name, exe, dir, icon, args) {
|
||||
const id = await SteamClient.Apps.AddShortcut(name, exe, dir, args);
|
||||
console.log("Shortcut created with ID:", id);
|
||||
await SteamClient.Apps.SetShortcutName(id, name);
|
||||
if (icon)
|
||||
await SteamClient.Apps.SetShortcutIcon(id, icon);
|
||||
if (args)
|
||||
await SteamClient.Apps.SetAppLaunchOptions(id, args);
|
||||
return { id };
|
||||
};
|
||||
|
||||
async function setGrid(id, i, ext, image) {
|
||||
await SteamClient.Apps.SetCustomArtworkForApp(id, image, ext, i);
|
||||
return true;
|
||||
};
|
||||
|
||||
async function removeShortcut(id) {
|
||||
await SteamClient.Apps.RemoveShortcut(+id);
|
||||
return true;
|
||||
};
|
||||
"""
|
||||
try:
|
||||
ws = websocket.create_connection(ws_url, timeout=5)
|
||||
js_args = ", ".join(orjson.dumps(arg).decode('utf-8') for arg in args)
|
||||
expression = f"{js_code} {js_cmd}({js_args});"
|
||||
payload = {
|
||||
"id": random.randint(0, 32767),
|
||||
"method": "Runtime.evaluate",
|
||||
"params": {
|
||||
"expression": expression,
|
||||
"awaitPromise": True,
|
||||
"returnByValue": True
|
||||
}
|
||||
}
|
||||
|
||||
ws.send(orjson.dumps(payload))
|
||||
response_str = ws.recv()
|
||||
ws.close()
|
||||
|
||||
response_data = orjson.loads(response_str)
|
||||
if "error" in response_data:
|
||||
logger.error(f"Ошибка выполнения JS в Steam: {response_data['error']['message']}")
|
||||
return None
|
||||
result = response_data.get('result', {}).get('result', {})
|
||||
if result.get('type') == 'object' and result.get('subtype') == 'error':
|
||||
logger.error(f"Ошибка выполнения JS в Steam: {result.get('description')}")
|
||||
return None
|
||||
return result.get('value')
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при взаимодействии с WebSocket Steam: {e}")
|
||||
return None
|
||||
|
||||
def add_to_steam(game_name: str, exec_line: str, cover_path: str) -> tuple[bool, str]:
|
||||
"""
|
||||
Add a non-Steam game to Steam via shortcuts.vdf with PortProton tag,
|
||||
@@ -846,45 +996,42 @@ export START_FROM_STEAM=1
|
||||
grid_dir = user_dir / "config" / "grid"
|
||||
os.makedirs(grid_dir, exist_ok=True)
|
||||
|
||||
backup_path = f"{steam_shortcuts_path}.backup"
|
||||
if os.path.exists(steam_shortcuts_path):
|
||||
try:
|
||||
shutil.copy2(steam_shortcuts_path, backup_path)
|
||||
logger.info(f"Created backup of shortcuts.vdf at {backup_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create backup of shortcuts.vdf: {e}")
|
||||
return (False, f"Failed to create backup of shortcuts.vdf: {e}")
|
||||
appid = None
|
||||
was_api_used = False
|
||||
|
||||
unique_string = f"{script_path}{game_name}"
|
||||
baseid = zlib.crc32(unique_string.encode('utf-8')) & 0xffffffff
|
||||
appid = baseid | 0x80000000
|
||||
if appid > 0x7FFFFFFF:
|
||||
aidvdf = appid - 0x100000000
|
||||
logger.info("Попытка добавления ярлыка через Steam CEF API...")
|
||||
api_response = call_steam_api(
|
||||
"createShortcut",
|
||||
game_name,
|
||||
script_path,
|
||||
str(Path(script_path).parent),
|
||||
icon_path,
|
||||
""
|
||||
)
|
||||
|
||||
if api_response and isinstance(api_response, dict) and 'id' in api_response:
|
||||
appid = api_response['id']
|
||||
was_api_used = True
|
||||
logger.info(f"Ярлык успешно добавлен через API. AppID: {appid}")
|
||||
else:
|
||||
aidvdf = appid
|
||||
logger.warning("Не удалось добавить ярлык через API. Используется запасной метод (запись в shortcuts.vdf).")
|
||||
backup_path = f"{steam_shortcuts_path}.backup"
|
||||
if os.path.exists(steam_shortcuts_path):
|
||||
try:
|
||||
shutil.copy2(steam_shortcuts_path, backup_path)
|
||||
logger.info(f"Created backup of shortcuts.vdf at {backup_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create backup of shortcuts.vdf: {e}")
|
||||
return (False, f"Failed to create backup of shortcuts.vdf: {e}")
|
||||
|
||||
steam_appid = None
|
||||
downloaded_count = 0
|
||||
total_covers = 4 # количество обложек
|
||||
unique_string = f"{script_path}{game_name}"
|
||||
baseid = zlib.crc32(unique_string.encode('utf-8')) & 0xffffffff
|
||||
appid = baseid | 0x80000000
|
||||
if appid > 0x7FFFFFFF:
|
||||
aidvdf = appid - 0x100000000
|
||||
else:
|
||||
aidvdf = appid
|
||||
|
||||
download_lock = threading.Lock()
|
||||
|
||||
def on_cover_download(cover_file: str, cover_type: str):
|
||||
nonlocal downloaded_count
|
||||
try:
|
||||
if cover_file and os.path.exists(cover_file):
|
||||
logger.info(f"Downloaded cover {cover_type} to {cover_file}")
|
||||
else:
|
||||
logger.warning(f"Failed to download cover {cover_type} for appid {steam_appid}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing cover {cover_type} for appid {steam_appid}: {e}")
|
||||
with download_lock:
|
||||
downloaded_count += 1
|
||||
if downloaded_count == total_covers:
|
||||
finalize_shortcut()
|
||||
|
||||
def finalize_shortcut():
|
||||
tags_dict = {'0': 'PortProton'}
|
||||
shortcut = {
|
||||
"appid": aidvdf,
|
||||
"AppName": game_name,
|
||||
@@ -899,7 +1046,7 @@ export START_FROM_STEAM=1
|
||||
"Devkit": 0,
|
||||
"DevkitGameID": "",
|
||||
"LastPlayTime": 0,
|
||||
"tags": tags_dict
|
||||
"tags": {'0': 'PortProton'}
|
||||
}
|
||||
logger.info(f"Shortcut entry to be written: {shortcut}")
|
||||
|
||||
@@ -929,6 +1076,7 @@ export START_FROM_STEAM=1
|
||||
|
||||
with open(steam_shortcuts_path, 'wb') as f:
|
||||
vdf.binary_dump({"shortcuts": shortcuts}, f)
|
||||
logger.info(f"Game '{game_name}' successfully added to Steam with covers")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update shortcuts.vdf: {e}")
|
||||
if os.path.exists(backup_path):
|
||||
@@ -937,34 +1085,54 @@ export START_FROM_STEAM=1
|
||||
logger.info("Restored shortcuts.vdf from backup due to update failure")
|
||||
except Exception as restore_err:
|
||||
logger.error(f"Failed to restore shortcuts.vdf from backup: {restore_err}")
|
||||
return (False, f"Failed to update shortcuts.vdf: {e}")
|
||||
appid = None
|
||||
|
||||
logger.info(f"Game '{game_name}' successfully added to Steam with covers")
|
||||
return (True, f"Game '{game_name}' added to Steam with covers")
|
||||
if not appid:
|
||||
return (False, "Не удалось создать ярлык ни одним из способов.")
|
||||
|
||||
steam_appid = None
|
||||
|
||||
def on_game_info(game_info: dict):
|
||||
nonlocal steam_appid
|
||||
steam_appid = game_info.get("appid")
|
||||
if not steam_appid or not isinstance(steam_appid, int):
|
||||
logger.info("No valid Steam appid found, skipping cover download")
|
||||
return finalize_shortcut()
|
||||
return
|
||||
logger.info(f"Найден Steam AppID {steam_appid} для загрузки обложек.")
|
||||
|
||||
# Обложки и имена, соответствующие bash-скрипту и твоим размерам
|
||||
cover_types = [
|
||||
(".jpg", "header.jpg"), # базовый, сохранится как AppId.jpg
|
||||
("p.jpg", "library_600x900_2x.jpg"), # сохранится как AppIdp.jpg
|
||||
("_hero.jpg", "library_hero.jpg"), # AppId_hero.jpg
|
||||
("_logo.png", "logo.png") # AppId_logo.png
|
||||
("p.jpg", "library_600x900_2x.jpg"),
|
||||
("_hero.jpg", "library_hero.jpg"),
|
||||
("_logo.png", "logo.png"),
|
||||
(".jpg", "header.jpg")
|
||||
]
|
||||
|
||||
for suffix, cover_type in cover_types:
|
||||
def on_cover_download(result_path: str | None, steam_name: str, index: int):
|
||||
try:
|
||||
if result_path and os.path.exists(result_path):
|
||||
logger.info(f"Downloaded cover {steam_name} to {result_path}")
|
||||
if was_api_used:
|
||||
try:
|
||||
with open(result_path, 'rb') as f:
|
||||
img_b64 = base64.b64encode(f.read()).decode('utf-8')
|
||||
logger.info(f"Применение обложки типа '{steam_name}' через API для AppID {appid}")
|
||||
ext = Path(steam_name).suffix.lstrip('.')
|
||||
call_steam_api("setGrid", appid, index, ext, img_b64)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при применении обложки '{steam_name}' через API: {e}")
|
||||
else:
|
||||
logger.warning(f"Failed to download cover {steam_name} for appid {steam_appid}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing cover {steam_name} for appid {steam_appid}: {e}")
|
||||
|
||||
for i, (suffix, steam_name) in enumerate(cover_types):
|
||||
cover_file = os.path.join(grid_dir, f"{appid}{suffix}")
|
||||
cover_url = f"https://cdn.cloudflare.steamstatic.com/steam/apps/{steam_appid}/{cover_type}"
|
||||
cover_url = f"https://cdn.cloudflare.steamstatic.com/steam/apps/{steam_appid}/{steam_name}"
|
||||
downloader.download_async(
|
||||
cover_url,
|
||||
cover_file,
|
||||
timeout=5,
|
||||
callback=lambda result, cfile=cover_file, ctype=cover_type: on_cover_download(cfile, ctype)
|
||||
callback=lambda result, index=i, name=steam_name: on_cover_download(result, name, index)
|
||||
)
|
||||
|
||||
get_steam_game_info_async(game_name, exec_line, on_game_info)
|
||||
@@ -1017,19 +1185,7 @@ def remove_from_steam(game_name: str, exec_line: str) -> tuple[bool, str]:
|
||||
logger.info(f"shortcuts.vdf not found at {steam_shortcuts_path}")
|
||||
return (False, f"Game '{game_name}' not found in Steam")
|
||||
|
||||
# Generate appid for identifying cover files
|
||||
unique_string = f"{script_path}{game_name}"
|
||||
baseid = zlib.crc32(unique_string.encode('utf-8')) & 0xffffffff
|
||||
appid = baseid | 0x80000000
|
||||
|
||||
# Create backup of shortcuts.vdf
|
||||
backup_path = f"{steam_shortcuts_path}.backup"
|
||||
try:
|
||||
shutil.copy2(steam_shortcuts_path, backup_path)
|
||||
logger.info(f"Created backup of shortcuts.vdf at {backup_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create backup of shortcuts.vdf: {e}")
|
||||
return (False, f"Failed to create backup of shortcuts.vdf: {e}")
|
||||
appid = None
|
||||
|
||||
# Load and modify shortcuts.vdf
|
||||
try:
|
||||
@@ -1043,37 +1199,51 @@ def remove_from_steam(game_name: str, exec_line: str) -> tuple[bool, str]:
|
||||
return (False, f"Failed to load shortcuts.vdf: {load_err}")
|
||||
|
||||
shortcuts = shortcuts_data.get("shortcuts", {})
|
||||
found = False
|
||||
new_shortcuts = {}
|
||||
index = 0
|
||||
|
||||
# Filter out the matching shortcut
|
||||
for _key, entry in shortcuts.items():
|
||||
if entry.get("AppName") == game_name and entry.get("Exe") == f'"{script_path}"':
|
||||
found = True
|
||||
appid = convert_steam_id(int(entry.get("appid")))
|
||||
logger.info(f"Found matching shortcut for '{game_name}' to remove")
|
||||
continue
|
||||
new_shortcuts[str(index)] = entry
|
||||
index += 1
|
||||
|
||||
if not found:
|
||||
if not appid:
|
||||
logger.info(f"Game '{game_name}' not found in Steam shortcuts")
|
||||
return (False, f"Game '{game_name}' not found in Steam")
|
||||
|
||||
# Save updated shortcuts.vdf
|
||||
try:
|
||||
with open(steam_shortcuts_path, 'wb') as f:
|
||||
vdf.binary_dump({"shortcuts": new_shortcuts}, f)
|
||||
logger.info(f"Successfully updated shortcuts.vdf, removed '{game_name}'")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update shortcuts.vdf: {e}")
|
||||
if os.path.exists(backup_path):
|
||||
try:
|
||||
shutil.copy2(backup_path, steam_shortcuts_path)
|
||||
logger.info("Restored shortcuts.vdf from backup due to update failure")
|
||||
except Exception as restore_err:
|
||||
logger.error(f"Failed to restore shortcuts.vdf from backup: {restore_err}")
|
||||
return (False, f"Failed to update shortcuts.vdf: {e}")
|
||||
api_response = call_steam_api("removeShortcut", appid)
|
||||
if api_response is not None: # API ответил, даже если ответ пустой
|
||||
logger.info(f"Ярлык для AppID {appid} успешно удален через API.")
|
||||
else:
|
||||
logger.warning("Не удалось удалить ярлык через API. Используется запасной метод (редактирование shortcuts.vdf).")
|
||||
|
||||
# Create backup of shortcuts.vdf
|
||||
backup_path = f"{steam_shortcuts_path}.backup"
|
||||
try:
|
||||
shutil.copy2(steam_shortcuts_path, backup_path)
|
||||
logger.info(f"Created backup of shortcuts.vdf at {backup_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create backup of shortcuts.vdf: {e}")
|
||||
return (False, f"Failed to create backup of shortcuts.vdf: {e}")
|
||||
|
||||
# Save updated shortcuts.vdf
|
||||
try:
|
||||
with open(steam_shortcuts_path, 'wb') as f:
|
||||
vdf.binary_dump({"shortcuts": new_shortcuts}, f)
|
||||
logger.info(f"Successfully updated shortcuts.vdf, removed '{game_name}'")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update shortcuts.vdf: {e}")
|
||||
if os.path.exists(backup_path):
|
||||
try:
|
||||
shutil.copy2(backup_path, steam_shortcuts_path)
|
||||
logger.info("Restored shortcuts.vdf from backup due to update failure")
|
||||
except Exception as restore_err:
|
||||
logger.error(f"Failed to restore shortcuts.vdf from backup: {restore_err}")
|
||||
return (False, f"Failed to update shortcuts.vdf: {e}")
|
||||
|
||||
# Delete cover files
|
||||
cover_files = [
|
||||
|
@@ -20,6 +20,8 @@ class SystemOverlay(QDialog):
|
||||
self.theme_manager = ThemeManager()
|
||||
self.setStyleSheet(self.theme.OVERLAY_WINDOW_STYLE)
|
||||
|
||||
self.script_path = "/usr/bin/portprotonqt-session-select"
|
||||
|
||||
# Make window stay on top and frameless
|
||||
self.setWindowFlags(
|
||||
Qt.WindowType.FramelessWindowHint |
|
||||
@@ -79,8 +81,7 @@ class SystemOverlay(QDialog):
|
||||
desktop_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE)
|
||||
desktop_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
desktop_button.clicked.connect(self.return_to_desktop)
|
||||
script_path = "/usr/bin/portprotonqt-session-select"
|
||||
script_exists = os.path.isfile(script_path)
|
||||
script_exists = os.path.isfile(self.script_path)
|
||||
desktop_button.setEnabled(script_exists)
|
||||
if not script_exists:
|
||||
desktop_button.setToolTip(_("portprotonqt-session-select file not found at /usr/bin/"))
|
||||
@@ -139,8 +140,8 @@ class SystemOverlay(QDialog):
|
||||
|
||||
def return_to_desktop(self):
|
||||
try:
|
||||
script_path = os.path.join(os.path.dirname(__file__), "portprotonqt-session-select")
|
||||
subprocess.run([script_path, "desktop"], check=True)
|
||||
QApplication.quit()
|
||||
subprocess.run([self.script_path, "desktop"], check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to return to desktop: {e}")
|
||||
QMessageBox.warning(self, _("Error"), _("Failed to return to desktop"))
|
||||
|
@@ -1 +0,0 @@
|
||||
<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="m3.3334 1c-1.2926 0-2.3333 1.0408-2.3333 2.3333v9.3333c0 1.2926 1.0408 2.3333 2.3333 2.3333h9.3333c1.2926 0 2.3333-1.0408 2.3333-2.3333v-9.3333c0-1.2926-1.0408-2.3333-2.3333-2.3333zm4.6667 2.4979c0.41261 0 0.75035 0.33774 0.75035 0.75035v3.0014h3.0014c0.41261 0 0.75035 0.33774 0.75035 0.75035s-0.33774 0.75035-0.75035 0.75035h-3.0014v3.0014c0 0.41261-0.33774 0.75035-0.75035 0.75035s-0.75035-0.33774-0.75035-0.75035v-3.0014h-3.0014c-0.41261 0-0.75035-0.33774-0.75035-0.75035s0.33774-0.75035 0.75035-0.75035h3.0014v-3.0014c0-0.41261 0.33774-0.75035 0.75035-0.75035z" fill="#fff" fill-rule="evenodd"/></svg>
|
Before Width: | Height: | Size: 734 B |
@@ -1 +0,0 @@
|
||||
<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="m12.13 2.2617-5.7389 5.7389 5.7389 5.7389-1.2604 1.2605-7-7 7-7z" fill="#fff"/></svg>
|
Before Width: | Height: | Size: 213 B |
@@ -1 +0,0 @@
|
||||
<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="m5.48 11.5 2.52-2.52 2.52 2.52 0.98-0.98-2.52-2.52 2.52-2.52-0.98-0.98-2.52 2.52-2.52-2.52-0.98 0.98 2.52 2.52-2.52 2.52zm2.52 3.5q-1.4525 0-2.73-0.55125t-2.2225-1.4962-1.4962-2.2225-0.55125-2.73 0.55125-2.73 1.4962-2.2225 2.2225-1.4962 2.73-0.55125 2.73 0.55125 2.2225 1.4962 1.4962 2.2225 0.55125 2.73-0.55125 2.73-1.4962 2.2225-2.2225 1.4962-2.73 0.55125zm0-1.4q2.345 0 3.9725-1.6275t1.6275-3.9725-1.6275-3.9725-3.9725-1.6275-3.9725 1.6275-1.6275 3.9725 1.6275 3.9725 3.9725 1.6275z"/></svg>
|
Before Width: | Height: | Size: 622 B |
@@ -1 +0,0 @@
|
||||
<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 11.5-7-7h14z" fill="#fff"/></svg>
|
Before Width: | Height: | Size: 164 B |
@@ -1 +0,0 @@
|
||||
<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="m7.02 11.22 4.935-4.935-0.98-0.98-3.955 3.955-1.995-1.995-0.98 0.98zm0.98 3.78q-1.4525 0-2.73-0.55125t-2.2225-1.4962-1.4962-2.2225-0.55125-2.73 0.55125-2.73 1.4962-2.2225 2.2225-1.4962 2.73-0.55125 2.73 0.55125 2.2225 1.4962 1.4962 2.2225 0.55125 2.73-0.55125 2.73-1.4962 2.2225-2.2225 1.4962-2.73 0.55125zm0-1.4q2.345 0 3.9725-1.6275t1.6275-3.9725-1.6275-3.9725-3.9725-1.6275-3.9725 1.6275-1.6275 3.9725 1.6275 3.9725 3.9725 1.6275z"/></svg>
|
Before Width: | Height: | Size: 570 B |
@@ -1 +0,0 @@
|
||||
<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="m0.52495 13.312c0.03087 1.3036 1.3683 2.0929 2.541 1.4723l9.4303-5.3381c0.51362-0.29103 0.86214-0.82453 0.86214-1.4496 0-0.62514-0.34835-1.1586-0.86214-1.4497l-9.4303-5.3304c-1.1727-0.62059-2.5111 0.16139-2.541 1.4647z" fill="#fff"/></svg>
|
Before Width: | Height: | Size: 367 B |
@@ -1 +0,0 @@
|
||||
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m31.994 2c-3.5777 0.00874-6.7581 2.209-8.0469 5.5059-6.026 1.9949-11.103 6.1748-14.25 11.576 0.3198-0.03567 0.64498-0.05624 0.9668-0.05664 1.8247 4.03e-4 3.6022 0.57036 5.0801 1.6406 2.053-2.9466 4.892-5.3048 8.2148-6.7754 1.3182 3.2847 4.496 5.4368 8.0352 5.4375 4.7859 5.76e-4 8.6634-3.8782 8.6641-8.6641 0-4.787-3.8774-8.6647-8.6641-8.6641zm14.148 8.4629c0.001493 0.068825 1.08e-4 0.13229 0 0.20117 0 2.0592-0.7314 4.0514-2.0664 5.6191 2.6029 1.9979 4.687 4.6357 6.0332 7.6758-0.03487 0.01246-0.051814 0.019533-0.089844 0.033204-3.239 1.3412-5.3501 4.5078-5.3496 8.0137-5.6e-5 3.5056 2.111 6.6588 5.3496 8 1.0511 0.43551 2.1787 0.66386 3.3164 0.66406 0.37838-3.45e-4 0.74796-0.028635 1.123-0.078125 4.3115-0.56733 7.5408-4.2367 7.541-8.5859-2.29e-4 -0.38522-0.026915-0.77645-0.078125-1.1582-0.41836-3.1252-2.5021-5.7755-5.4395-6.9219-1.8429-5.5649-5.5319-10.293-10.34-13.463zm-35.479 12.867v0.011719c-4.7868-5.8e-4 -8.6645 3.8772-8.6641 8.6641 5.184e-4 3.5694 2.1832 6.7701 5.5078 8.0684 1.9494 5.8885 5.9701 10.844 11.191 14.004-0.02187-0.24765-0.032753-0.49357-0.033203-0.74219 0.0011-1.8915 0.6215-3.7301 1.7656-5.2363-2.8389-2.0415-5.1101-4.8211-6.541-8.0586-0.010718 0.00405-0.023854 0.005164-0.035156 0.007812 3.2771-1.2961 5.4686-4.4709 5.4746-8.043 7e-6 -4.787-3.8793-8.6762-8.666-8.6758zm18.264 2.9746c-1.1128 0.59718-2.0251 1.5031-2.627 2.6133 0.49567 0.91964 0.77734 1.9689 0.77734 3.082 0 1.1135-0.28142 2.168-0.77734 3.0879 0.60218 1.1114 1.5189 2.0216 2.6328 2.6191 0.9175-0.49216 1.9588-0.77148 3.0684-0.77148 1.1032 0 2.1508 0.27284 3.0645 0.75976 1.1182-0.60366 2.032-1.5238 2.6309-2.6445-0.48449-0.91196-0.76367-1.9505-0.76367-3.0508 0-1.1 0.27944-2.1331 0.76367-3.0449-0.59834-1.1194-1.5143-2.0409-2.6309-2.6445-0.91432 0.48781-1.9603 0.76367-3.0645 0.76367-1.1106 3e-6 -2.1561-0.27646-3.0742-0.76953zm19.328 17.041c-2.0547 2.9472-4.8918 5.3038-8.2148 6.7754-1.3171-3.2891-4.504-5.4498-8.0469-5.4492-4.7846 0.0017-8.6634 3.8796-8.6641 8.6641 0 4.7856 3.8788 8.6627 8.6641 8.6641 3.5711 7.48e-4 6.782-2.179 8.0801-5.5059 6.026-1.9954 11.081-6.1633 14.227-11.564-0.3202 0.03625-0.64263 0.05628-0.96484 0.05664-1.822-2.87e-4 -3.6033-0.57345-5.0801-1.6406z"/></svg>
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1 +0,0 @@
|
||||
<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 11.5-4.375-4.375 1.225-1.2688 2.275 2.275v-7.1312h1.75v7.1312l2.275-2.275 1.225 1.2688zm-5.25 3.5q-0.72188 0-1.2359-0.51406-0.51406-0.51406-0.51406-1.2359v-2.625h1.75v2.625h10.5v-2.625h1.75v2.625q0 0.72188-0.51406 1.2359-0.51406 0.51406-1.2359 0.51406z"/></svg>
|
Before Width: | Height: | Size: 392 B |
@@ -1 +0,0 @@
|
||||
<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="m14.396 13.564-2.5415-2.5415c0.94769-1.0626 1.5364-2.4698 1.5364-4.0062 0-3.3169-2.6995-6.0164-6.0164-6.0164-3.3169 0-6.0164 2.6995-6.0164 6.0164s2.6995 6.0164 6.0164 6.0164c1.1774 0 2.2688-0.34469 3.1877-0.91896l2.6421 2.6421c0.15799 0.15799 0.37342 0.24416 0.58871 0.24416 0.21544 0 0.43079-0.08617 0.58874-0.24416 0.34469-0.33033 0.34469-0.86155 0.01512-1.1918zm-11.358-6.5477c0-2.3836 1.9384-4.3364 4.3364-4.3364 2.3979 0 4.3221 1.9528 4.3221 4.3364s-1.9384 4.3364-4.3221 4.3364-4.3364-1.9384-4.3364-4.3364z" fill="#fff"/></svg>
|
Before Width: | Height: | Size: 660 B |
Before Width: | Height: | Size: 7.9 KiB |
@@ -1 +0,0 @@
|
||||
<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="m14.125 8.875h-1.75c-0.48386 0-0.87498-0.39113-0.87498-0.87498 0-0.48389 0.39113-0.87502 0.87498-0.87502h1.75c0.48386 0 0.87498 0.39113 0.87498 0.87502 0 0.48386-0.39113 0.87498-0.87498 0.87498zm-2.4133-3.3494c-0.34211 0.34211-0.89513 0.34211-1.2372 0-0.34211-0.34214-0.34211-0.89513 0-1.2373l1.2372-1.2372c0.34211-0.34211 0.89513-0.34211 1.2372 0 0.34211 0.34214 0.34211 0.89513 0 1.2372zm-3.7117 9.4744c-0.48389 0-0.87501-0.39113-0.87501-0.87498v-1.75c0-0.48386 0.39113-0.87498 0.87501-0.87498 0.48386 0 0.87498 0.39113 0.87498 0.87498v1.75c0 0.48386-0.39113 0.87498-0.87498 0.87498zm0-10.5c-0.48389 0-0.87501-0.39113-0.87501-0.87498v-1.75c0-0.48386 0.39113-0.87498 0.87501-0.87498 0.48386 0 0.87498 0.39113 0.87498 0.87498v1.75c0 0.48386-0.39113 0.87498-0.87498 0.87498zm-3.7117 8.449c-0.34211 0.34211-0.8951 0.34211-1.2372 0-0.34211-0.34211-0.34211-0.89513 0-1.2372l1.2372-1.2372c0.34214-0.34211 0.89513-0.34211 1.2373 0 0.34211 0.34211 0.34211 0.89513 0 1.2372zm0-7.4234-1.2372-1.2373c-0.34211-0.34211-0.34211-0.8951 0-1.2372 0.34214-0.34211 0.89513-0.34211 1.2372 0l1.2373 1.2372c0.34211 0.34214 0.34211 0.89513 0 1.2373-0.34214 0.34211-0.89513 0.34211-1.2373 0zm0.21165 2.4744c0 0.48386-0.39113 0.87498-0.87498 0.87498h-1.75c-0.48386 0-0.87498-0.39113-0.87498-0.87498 0-0.48389 0.39113-0.87501 0.87498-0.87501h1.75c0.48386 0 0.87498 0.39113 0.87498 0.87501zm7.2117 2.4744 1.2372 1.2372c0.34211 0.34211 0.34211 0.89598 0 1.2372-0.34211 0.34126-0.89513 0.34211-1.2372 0l-1.2372-1.2372c-0.34211-0.34211-0.34211-0.89513 0-1.2372s0.89513-0.34211 1.2372 0z" fill="#fff"/></svg>
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1 +0,0 @@
|
||||
<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="m7.9881 1.001c-3.6755 0-6.6898 2.835-6.9751 6.4383l3.752 1.5505c0.31806-0.21655 0.70174-0.34431 1.1152-0.34431 0.03671 0 0.07309 0.0017 0.1098 0.0033l1.6691-2.4162v-0.03456c0-1.4556 1.183-2.639 2.639-2.639 1.4547 0 2.639 1.1847 2.639 2.6407 0 1.456-1.1843 2.6395-2.639 2.6395h-0.06118l-2.3778 1.6979c0 0.03009 0.0017 0.06118 0.0017 0.09276 0 1.0938-0.88376 1.981-1.9776 1.981-0.95374 0-1.7592-0.68426-1.9429-1.5907l-2.6863-1.1126c0.83168 2.9383 3.5292 5.0924 6.7335 5.0924 3.8657 0 6.9995-3.1343 6.9995-7s-3.1343-7-6.9995-7zm-2.5895 10.623-0.85924-0.35569c0.15262 0.31676 0.4165 0.58274 0.7665 0.72931 0.75645 0.31456 1.6293-0.04415 1.9438-0.80194 0.15361-0.3675 0.15394-0.76956 0.0033-1.1371-0.15097-0.3675-0.4375-0.65408-0.80324-0.80674-0.364-0.1518-0.7525-0.14535-1.0955-0.01753l0.88855 0.3675c0.55782 0.23318 0.82208 0.875 0.58845 1.4319-0.23145 0.55826-0.87326 0.8225-1.4315 0.59019zm6.6587-5.4267c0-0.96948-0.78926-1.7587-1.7587-1.7587-0.97126 0-1.7587 0.78926-1.7587 1.7587 0 0.97126 0.7875 1.7587 1.7587 1.7587 0.96994 0 1.7587-0.7875 1.7587-1.7587zm-3.0756-0.0033c0-0.73019 0.59106-1.3217 1.3212-1.3217 0.72844 0 1.3217 0.59148 1.3217 1.3217 0 0.72976-0.59326 1.3213-1.3217 1.3213-0.73106 0-1.3212-0.5915-1.3212-1.3213z" fill="#fff"/></svg>
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
||||
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="14" height="14" rx="2.8" fill="#fff" fill-rule="evenodd"/></svg>
|
Before Width: | Height: | Size: 208 B |
@@ -1 +0,0 @@
|
||||
<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="m1 11.5 7-7 7 7z" fill="#fff"/></svg>
|
Before Width: | Height: | Size: 165 B |
@@ -1 +0,0 @@
|
||||
<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 15q-1.4583 0-2.7319-0.55417-1.2735-0.55417-2.2167-1.4972-0.94313-0.94306-1.4972-2.2167t-0.55417-2.7319q-7.699e-5 -1.4583 0.55417-2.7319 0.55424-1.2736 1.4972-2.2167 0.94298-0.94306 2.2167-1.4972 1.2737-0.55417 2.7319-0.55417 1.5944 0 3.0237 0.68056 1.4292 0.68056 2.4208 1.925v-1.8278h1.5556v4.6667h-4.6667v-1.5556h2.1389q-0.79722-1.0889-1.9639-1.7111-1.1667-0.62222-2.5083-0.62222-2.275 0-3.8596 1.5848t-1.5848 3.8596q-1.55e-4 2.2748 1.5848 3.8596 1.585 1.5848 3.8596 1.5848 2.0417 0 3.5681-1.3222t1.7985-3.3444h1.5944q-0.29167 2.6639-2.2848 4.443-1.9931 1.7791-4.6763 1.7792z"/></svg>
|
Before Width: | Height: | Size: 717 B |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.6 MiB |
Before Width: | Height: | Size: 475 KiB |
Before Width: | Height: | Size: 151 KiB |
@@ -1,5 +0,0 @@
|
||||
[Metainfo]
|
||||
author = BlackSnaker
|
||||
author_link =
|
||||
description = Стандартная тема PortProtonQt (светлый вариант)
|
||||
name = Light
|
@@ -1,699 +0,0 @@
|
||||
from portprotonqt.theme_manager import ThemeManager
|
||||
from portprotonqt.config_utils import read_theme_from_config
|
||||
|
||||
theme_manager = ThemeManager()
|
||||
current_theme_name = read_theme_from_config()
|
||||
|
||||
# КОНСТАНТЫ
|
||||
favoriteLabelSize = 48, 48
|
||||
pixmapsScaledSize = 60, 60
|
||||
|
||||
GAME_CARD_ANIMATION = {
|
||||
# Ширина обводки карточки в состоянии покоя (без наведения или фокуса).
|
||||
# Влияет на толщину рамки вокруг карточки, когда она не выделена.
|
||||
# Значение в пикселях.
|
||||
"default_border_width": 2,
|
||||
|
||||
# Ширина обводки при наведении курсора.
|
||||
# Увеличивает толщину рамки, когда курсор находится над карточкой.
|
||||
# Значение в пикселях.
|
||||
"hover_border_width": 8,
|
||||
|
||||
# Ширина обводки при фокусе (например, при выборе с клавиатуры).
|
||||
# Увеличивает толщину рамки, когда карточка в фокусе.
|
||||
# Значение в пикселях.
|
||||
"focus_border_width": 12,
|
||||
|
||||
# Минимальная ширина обводки во время пульсирующей анимации.
|
||||
# Определяет минимальную толщину рамки при пульсации (анимация "дыхания").
|
||||
# Значение в пикселях.
|
||||
"pulse_min_border_width": 8,
|
||||
|
||||
# Максимальная ширина обводки во время пульсирующей анимации.
|
||||
# Определяет максимальную толщину рамки при пульсации.
|
||||
# Значение в пикселях.
|
||||
"pulse_max_border_width": 10,
|
||||
|
||||
# Длительность анимации изменения толщины обводки (например, при наведении или фокусе).
|
||||
# Влияет на скорость перехода от одной ширины обводки к другой.
|
||||
# Значение в миллисекундах.
|
||||
"thickness_anim_duration": 300,
|
||||
|
||||
# Длительность одного цикла пульсирующей анимации.
|
||||
# Определяет, как быстро рамка "пульсирует" между min и max значениями.
|
||||
# Значение в миллисекундах.
|
||||
"pulse_anim_duration": 800,
|
||||
|
||||
# Длительность анимации вращения градиента.
|
||||
# Влияет на скорость, с которой градиентная обводка вращается вокруг карточки.
|
||||
# Значение в миллисекундах.
|
||||
"gradient_anim_duration": 3000,
|
||||
|
||||
# Начальный угол градиента (в градусах).
|
||||
# Определяет начальную точку вращения градиента при старте анимации.
|
||||
"gradient_start_angle": 360,
|
||||
|
||||
# Конечный угол градиента (в градусах).
|
||||
# Определяет конечную точку вращения градиента.
|
||||
# Значение 0 означает полный поворот на 360 градусов.
|
||||
"gradient_end_angle": 0,
|
||||
|
||||
# Тип кривой сглаживания для анимации увеличения обводки (при наведении/фокусе).
|
||||
# Влияет на "чувство" анимации (например, плавное ускорение или замедление).
|
||||
# Возможные значения: строки, соответствующие QEasingCurve.Type (например, "OutBack", "InOutQuad").
|
||||
"thickness_easing_curve": "OutBack",
|
||||
|
||||
# Тип кривой сглаживания для анимации уменьшения обводки (при уходе курсора/потере фокуса).
|
||||
# Влияет на "чувство" возврата к исходной ширине обводки.
|
||||
"thickness_easing_curve_out": "InBack",
|
||||
|
||||
# Цвета градиента для анимированной обводки.
|
||||
# Список словарей, где каждый словарь задает позицию (0.0–1.0) и цвет в формате hex.
|
||||
# Влияет на внешний вид обводки при наведении или фокусе.
|
||||
"gradient_colors": [
|
||||
{"position": 0, "color": "#00fff5"}, # Начальный цвет (циан)
|
||||
{"position": 0.33, "color": "#FF5733"}, # Цвет на 33% (оранжевый)
|
||||
{"position": 0.66, "color": "#9B59B6"}, # Цвет на 66% (пурпурный)
|
||||
{"position": 1, "color": "#00fff5"} # Конечный цвет (возвращение к циану)
|
||||
]
|
||||
}
|
||||
|
||||
# СТИЛЬ ШАПКИ ГЛАВНОГО ОКНА
|
||||
MAIN_WINDOW_HEADER_STYLE = """
|
||||
QFrame {
|
||||
background: transparent;
|
||||
border: 10px solid rgba(255, 255, 255, 0.10);
|
||||
border-bottom: 0px solid rgba(255, 255, 255, 0.15);
|
||||
border-top-left-radius: 30px;
|
||||
border-top-right-radius: 30px;
|
||||
border: none;
|
||||
}
|
||||
"""
|
||||
|
||||
# СТИЛЬ ЗАГОЛОВКА (ЛОГО) В ШАПКЕ
|
||||
TITLE_LABEL_STYLE = """
|
||||
QLabel {
|
||||
font-family: 'RASKHAL';
|
||||
font-size: 38px;
|
||||
margin: 0 0 0 0;
|
||||
color: #007AFF;
|
||||
}
|
||||
"""
|
||||
|
||||
# СТИЛЬ ОБЛАСТИ НАВИГАЦИИ (КНОПКИ ВКЛАДОК)
|
||||
NAV_WIDGET_STYLE = """
|
||||
QWidget {
|
||||
background: #ffffff;
|
||||
border-bottom: 0px solid rgba(0, 0, 0, 0.10);
|
||||
}
|
||||
"""
|
||||
|
||||
# СТИЛЬ КНОПОК ВКЛАДОК НАВИГАЦИИ
|
||||
NAV_BUTTON_STYLE = """
|
||||
NavLabel {
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
|
||||
stop:0 rgba(242, 242, 242, 0.5),
|
||||
stop:1 rgba(232, 232, 232, 0.5));
|
||||
padding: 10px 10px;
|
||||
margin: 10px 0 10px 10px;
|
||||
color: #333333;
|
||||
font-size: 16px;
|
||||
font-family: 'Poppins';
|
||||
text-transform: uppercase;
|
||||
border: 1px solid rgba(179, 179, 179, 0.4);
|
||||
border-radius: 15px;
|
||||
}
|
||||
NavLabel[checked = true] {
|
||||
background: rgba(0,122,255,0.25);
|
||||
color: #002244;
|
||||
font-weight: bold;
|
||||
border-radius: 15px;
|
||||
}
|
||||
NavLabel:hover {
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
|
||||
stop:0 rgba(0,122,255,0.12),
|
||||
stop:1 rgba(0,122,255,0.08));
|
||||
color: #002244;
|
||||
}
|
||||
"""
|
||||
|
||||
# ГЛОБАЛЬНЫЙ СТИЛЬ ДЛЯ ОКНА (ФОН) И QLabel
|
||||
MAIN_WINDOW_STYLE = """
|
||||
QMainWindow {
|
||||
background: none;
|
||||
}
|
||||
QLabel {
|
||||
color: #333333;
|
||||
}
|
||||
"""
|
||||
|
||||
# СТИЛЬ ПОЛЯ ПОИСКА
|
||||
SEARCH_EDIT_STYLE = """
|
||||
QLineEdit {
|
||||
background-color: rgba(30, 30, 30, 0.50);
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
border-radius: 10px;
|
||||
padding: 7px 14px;
|
||||
font-family: 'Poppins';
|
||||
font-size: 16px;
|
||||
color: #ffffff;
|
||||
}
|
||||
QLineEdit:focus {
|
||||
border: 1px solid rgba(0,122,255,0.25);
|
||||
}
|
||||
"""
|
||||
|
||||
# ОТКЛЮЧАЕМ РАМКУ У QScrollArea
|
||||
SCROLL_AREA_STYLE = """
|
||||
QWidget {
|
||||
background: transparent;
|
||||
}
|
||||
QScrollBar:vertical {
|
||||
width: 10px;
|
||||
border: 0px solid;
|
||||
border-radius: 5px;
|
||||
background: rgba(20, 20, 20, 0.30);
|
||||
}
|
||||
QScrollBar::handle:vertical {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border: 0px solid;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QScrollBar::add-line:vertical {
|
||||
border: 0px solid;
|
||||
background: none;
|
||||
}
|
||||
QScrollBar::sub-line:vertical {
|
||||
border: 0px solid;
|
||||
background: none;
|
||||
}
|
||||
QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical {
|
||||
border: 0px solid;
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
background: none;
|
||||
}
|
||||
QScrollBar:horizontal {
|
||||
height: 10px;
|
||||
border: 0px solid;
|
||||
border-radius: 5px;
|
||||
background: rgba(20, 20, 20, 0.30);
|
||||
}
|
||||
QScrollBar::handle:horizontal {
|
||||
background: #bebebe;
|
||||
border: 0px solid;
|
||||
border-radius: 5px;
|
||||
}
|
||||
QScrollBar::add-line:horizontal {
|
||||
border: 0px solid;
|
||||
background: none;
|
||||
}
|
||||
QScrollBar::sub-line:horizontal {
|
||||
border: 0px solid;
|
||||
background: none;
|
||||
}
|
||||
QScrollBar::up-arrow:horizontal, QScrollBar::down-arrow:horizontal {
|
||||
border: 0px solid;
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
background: none;
|
||||
}
|
||||
"""
|
||||
|
||||
# SLIDER_SIZE_STYLE
|
||||
SLIDER_SIZE_STYLE= """
|
||||
QWidget {
|
||||
background: transparent;
|
||||
height: 25px;
|
||||
}
|
||||
QSlider::groove:horizontal {
|
||||
border: 0px solid;
|
||||
border-radius: 3px;
|
||||
height: 6px; /* the groove expands to the size of the slider by default. by giving it a height, it has a fixed size */
|
||||
background: rgba(20, 20, 20, 0.30);
|
||||
margin: 6px 0;
|
||||
}
|
||||
QSlider::handle:horizontal {
|
||||
background: #bebebe;
|
||||
border: 0px solid;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin: -6px 0; /* handle is placed by default on the contents rect of the groove. Expand outside the groove */
|
||||
border-radius: 9px;
|
||||
}
|
||||
"""
|
||||
|
||||
# СТИЛЬ ОБЛАСТИ ДЛЯ КАРТОЧЕК ИГР (QWidget)
|
||||
LIST_WIDGET_STYLE = """
|
||||
QWidget {
|
||||
background: none;
|
||||
border: 0px solid rgba(255, 255, 255, 0.10);
|
||||
border-radius: 25px;
|
||||
}
|
||||
"""
|
||||
|
||||
# ЗАГОЛОВОК "БИБЛИОТЕКА" НА ВКЛАДКЕ
|
||||
INSTALLED_TAB_TITLE_STYLE = "font-family: 'Poppins'; font-size: 24px; color: #232627;"
|
||||
|
||||
# СТИЛЬ КНОПОК "СОХРАНЕНИЯ, ПРИМЕНЕНИЯ И Т.Д."
|
||||
ACTION_BUTTON_STYLE = """
|
||||
QPushButton {
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
|
||||
stop:0 rgba(242, 242, 242, 0.5),
|
||||
stop:1 rgba(232, 232, 232, 0.5));
|
||||
border: 1px solid rgba(179, 179, 179, 0.4);
|
||||
border-radius: 10px;
|
||||
color: #232627;
|
||||
font-size: 16px;
|
||||
font-family: 'Poppins';
|
||||
padding: 8px 16px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: rgba(0,122,255,0.25);
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background: rgba(0,122,255,0.25);
|
||||
}
|
||||
"""
|
||||
|
||||
# ТЕКСТОВЫЕ СТИЛИ: ЗАГОЛОВКИ И ОСНОВНОЙ КОНТЕНТ
|
||||
TAB_TITLE_STYLE = "font-family: 'Poppins'; font-size: 24px; color: #232627; background-color: none;"
|
||||
CONTENT_STYLE = """
|
||||
QLabel {
|
||||
font-family: 'Poppins';
|
||||
font-size: 16px;
|
||||
color: #232627;
|
||||
background-color: none;
|
||||
border-bottom: 1px solid rgba(165, 165, 165, 0.7);
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
"""
|
||||
|
||||
# СТИЛЬ ОСНОВНЫХ СТРАНИЦ
|
||||
# LIBRARY_WIDGET_STYLE
|
||||
LIBRARY_WIDGET_STYLE= """
|
||||
QWidget {
|
||||
background: qlineargradient(spread:pad, x1:0.162, y1:0.0313409, x2:1, y2:1, stop:0 rgba(215, 235, 255, 255), stop:1 rgba(253, 252, 255, 255));
|
||||
border-radius: 0px;
|
||||
}
|
||||
"""
|
||||
|
||||
# CONTAINER_STYLE
|
||||
CONTAINER_STYLE= """
|
||||
QWidget {
|
||||
background-color: none;
|
||||
}
|
||||
"""
|
||||
|
||||
# OTHER_PAGES_WIDGET_STYLE
|
||||
OTHER_PAGES_WIDGET_STYLE= """
|
||||
QWidget {
|
||||
background: qlineargradient(spread:pad, x1:0.162, y1:0.0313409, x2:1, y2:1, stop:0 rgba(215, 235, 255, 255), stop:1 rgba(253, 252, 255, 255));
|
||||
border-radius: 0px;
|
||||
}
|
||||
"""
|
||||
|
||||
# CAROUSEL_WIDGET_STYLE
|
||||
CAROUSEL_WIDGET_STYLE= """
|
||||
QWidget {
|
||||
background: qlineargradient(spread:pad, x1:0.099, y1:0.119, x2:0.917, y2:0.936149, stop:0 rgba(215, 235, 255, 255), stop:1 rgba(217, 193, 255, 255));
|
||||
border-radius: 0px;
|
||||
}
|
||||
"""
|
||||
|
||||
# ФОН ДЛЯ ДЕТАЛЬНОЙ СТРАНИЦЫ, ЕСЛИ ОБЛОЖКА НЕ ЗАГРУЖЕНА
|
||||
DETAIL_PAGE_NO_COVER_STYLE = "background: rgba(20,20,20,0.95); border-radius: 15px;"
|
||||
|
||||
# СТИЛЬ КНОПКИ "ДОБАВИТЬ ИГРУ" И "НАЗАД" НА ДЕТАЛЬНОЙ СТРАНИЦЕ И БИБЛИОТЕКИ
|
||||
ADDGAME_BACK_BUTTON_STYLE = """
|
||||
QPushButton {
|
||||
background: rgba(20, 20, 20, 0.40);
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
border-radius: 10px;
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
font-family: 'Poppins';
|
||||
padding: 4px 16px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: rgba(0,122,255,0.25);
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background: rgba(0,122,255,0.25);
|
||||
}
|
||||
"""
|
||||
|
||||
# ОСНОВНОЙ ФРЕЙМ ДЕТАЛЕЙ ИГРЫ
|
||||
DETAIL_CONTENT_FRAME_STYLE = """
|
||||
QFrame {
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
|
||||
stop:0 rgba(20, 20, 20, 0.40),
|
||||
stop:1 rgba(20, 20, 20, 0.35));
|
||||
border: 0px solid rgba(255, 255, 255, 0.10);
|
||||
border-radius: 15px;
|
||||
}
|
||||
"""
|
||||
|
||||
# ФРЕЙМ ПОД ОБЛОЖКОЙ
|
||||
COVER_FRAME_STYLE = """
|
||||
QFrame {
|
||||
background: rgba(30, 30, 30, 0.80);
|
||||
border-radius: 15px;
|
||||
border: 0px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
"""
|
||||
|
||||
# СКРУГЛЕНИЕ LABEL ПОД ОБЛОЖКУ
|
||||
COVER_LABEL_STYLE = "border-radius: 100px;"
|
||||
|
||||
# ВИДЖЕТ ДЕТАЛЕЙ (ТЕКСТ, ОПИСАНИЕ)
|
||||
DETAILS_WIDGET_STYLE = "background: rgba(20,20,20,0.40); border-radius: 15px; padding: 10px;"
|
||||
|
||||
# НАЗВАНИЕ (ЗАГОЛОВОК) НА ДЕТАЛЬНОЙ СТРАНИЦЕ
|
||||
DETAIL_PAGE_TITLE_STYLE = "font-family: 'Orbitron'; font-size: 32px; color: #007AFF;"
|
||||
|
||||
# ЛИНИЯ-РАЗДЕЛИТЕЛЬ
|
||||
DETAIL_PAGE_LINE_STYLE = "color: rgba(255,255,255,0.12); margin: 10px 0;"
|
||||
|
||||
# ТЕКСТ ОПИСАНИЯ
|
||||
DETAIL_PAGE_DESC_STYLE = "font-family: 'Poppins'; font-size: 16px; color: #ffffff; line-height: 1.5;"
|
||||
|
||||
# СТИЛЬ КНОПКИ "ИГРАТЬ"
|
||||
PLAY_BUTTON_STYLE = """
|
||||
QPushButton {
|
||||
background: rgba(20, 20, 20, 0.40);
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
border-radius: 10px;
|
||||
font-size: 18px;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
font-family: 'Orbitron';
|
||||
padding: 8px 16px;
|
||||
min-width: 120px;
|
||||
min-height: 40px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: rgba(0,122,255,0.25);
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background: rgba(0,122,255,0.25);
|
||||
}
|
||||
"""
|
||||
|
||||
# СТИЛЬ КНОПКИ "ОБЗОР..." В ДИАЛОГЕ "ДОБАВИТЬ ИГРУ"
|
||||
DIALOG_BROWSE_BUTTON_STYLE = """
|
||||
QPushButton {
|
||||
background: rgba(20, 20, 20, 0.40);
|
||||
border: 0px solid rgba(255, 255, 255, 0.20);
|
||||
border-radius: 15px;
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
|
||||
stop:0 rgba(0,122,255,0.20),
|
||||
stop:1 rgba(0,122,255,0.15));
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background: rgba(20, 20, 20, 0.60);
|
||||
border: 0px solid rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
"""
|
||||
|
||||
# СТИЛЬ КАРТОЧКИ ИГРЫ (GAMECARD)
|
||||
GAME_CARD_WINDOW_STYLE = """
|
||||
QFrame {
|
||||
border-radius: 20px;
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||
stop:0 rgba(255, 255, 255, 0.3),
|
||||
stop:1 rgba(249, 249, 249, 0.3));
|
||||
border: 0px solid rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
"""
|
||||
|
||||
# НАЗВАНИЕ В КАРТОЧКЕ (QLabel)
|
||||
GAME_CARD_NAME_LABEL_STYLE = """
|
||||
QLabel {
|
||||
color: #333333;
|
||||
font-family: 'Orbitron';
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0,
|
||||
stop:0 rgba(242, 242, 242, 0.5),
|
||||
stop:1 rgba(232, 232, 232, 0.5));
|
||||
border-radius: 20px;
|
||||
padding: 7px;
|
||||
qproperty-wordWrap: true;
|
||||
}
|
||||
"""
|
||||
|
||||
# ДОПОЛНИТЕЛЬНЫЕ СТИЛИ ИНФОРМАЦИИ НА СТРАНИЦЕ ИГР
|
||||
LAST_LAUNCH_TITLE_STYLE = "font-family: 'Poppins'; font-size: 11px; color: #bbbbbb; text-transform: uppercase; letter-spacing: 0.75px; margin-bottom: 2px;"
|
||||
LAST_LAUNCH_VALUE_STYLE = "font-family: 'Poppins'; font-size: 13px; color: #ffffff; font-weight: 600; letter-spacing: 0.75px;"
|
||||
PLAY_TIME_TITLE_STYLE = "font-family: 'Poppins'; font-size: 11px; color: #bbbbbb; text-transform: uppercase; letter-spacing: 0.75px; margin-bottom: 2px;"
|
||||
PLAY_TIME_VALUE_STYLE = "font-family: 'Poppins'; font-size: 13px; color: #ffffff; font-weight: 600; letter-spacing: 0.75px;"
|
||||
GAMEPAD_SUPPORT_VALUE_STYLE = """
|
||||
font-family: 'Poppins'; font-size: 12px; color: #00ff00;
|
||||
font-weight: bold; background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 5px; padding: 4px 8px;
|
||||
"""
|
||||
|
||||
# СТИЛИ ПОЛНОЭКРАНОГО ПРЕВЬЮ СКРИНШОТОВ ТЕМЫ
|
||||
PREV_BUTTON_STYLE="background-color: rgba(0, 0, 0, 0.5); color: white; border: none;"
|
||||
NEXT_BUTTON_STYLE="background-color: rgba(0, 0, 0, 0.5); color: white; border: none;"
|
||||
CAPTION_LABEL_STYLE="color: white; font-size: 16px;"
|
||||
|
||||
# СТИЛИ БЕЙДЖА PROTONDB НА КАРТОЧКЕ
|
||||
def get_protondb_badge_style(tier):
|
||||
tier = tier.lower()
|
||||
tier_colors = {
|
||||
"platinum": {"background": "rgba(255,255,255,0.9)", "color": "black"},
|
||||
"gold": {"background": "rgba(253,185,49,0.7)", "color": "black"},
|
||||
"silver": {"background": "rgba(169,169,169,0.8)", "color": "black"},
|
||||
"bronze": {"background": "rgba(205,133,63,0.7)", "color": "black"},
|
||||
"borked": {"background": "rgba(255,0,0,0.7)", "color": "black"},
|
||||
"pending": {"background": "rgba(160,82,45,0.7)", "color": "black"}
|
||||
}
|
||||
colors = tier_colors.get(tier, {"background": "rgba(0, 0, 0, 0.5)", "color": "white"})
|
||||
return f"""
|
||||
qproperty-alignment: AlignCenter;
|
||||
background-color: {colors["background"]};
|
||||
color: {colors["color"]};
|
||||
border-radius: 5px;
|
||||
font-family: 'Poppins';
|
||||
font-weight: bold;
|
||||
"""
|
||||
|
||||
def get_anticheat_badge_style(status):
|
||||
status = status.lower()
|
||||
status_colors = {
|
||||
"supported": {"background": "rgba(102, 168, 15, 0.7)", "color": "black"},
|
||||
"running": {"background": "rgba(25, 113, 194, 0.7)", "color": "black"},
|
||||
"planned": {"background": "rgba(156, 54, 181, 0.7)", "color": "black"},
|
||||
"broken": {"background": "rgba(232, 89, 12, 0.7)", "color": "black"},
|
||||
"denied": {"background": "rgba(224, 49, 49, 0.7)", "color": "black"}
|
||||
}
|
||||
colors = status_colors.get(status, {"background": "rgba(0, 0, 0, 0.5)", "color": "white"})
|
||||
return f"""
|
||||
qproperty-alignment: AlignCenter;
|
||||
background-color: {colors["background"]};
|
||||
color: {colors["color"]};
|
||||
border-radius: 5px;
|
||||
font-family: 'Poppins';
|
||||
font-weight: bold;
|
||||
"""
|
||||
|
||||
# СТИЛИ БЕЙДЖА STEAM
|
||||
STEAM_BADGE_STYLE= """
|
||||
qproperty-alignment: AlignCenter;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
font-family: 'Poppins';
|
||||
font-weight: bold;
|
||||
"""
|
||||
|
||||
# Favorite Star
|
||||
FAVORITE_LABEL_STYLE = "color: gold; font-size: 32px; background: transparent; border: none;"
|
||||
|
||||
# СТИЛИ ДЛЯ QMessageBox (ОКНА СООБЩЕНИЙ)
|
||||
MESSAGE_BOX_STYLE = """
|
||||
QMessageBox {
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
|
||||
stop:0 rgba(40, 40, 40, 0.95),
|
||||
stop:1 rgba(25, 25, 25, 0.95));
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 12px;
|
||||
}
|
||||
QMessageBox QLabel {
|
||||
color: #ffffff;
|
||||
font-family: 'Poppins';
|
||||
font-size: 16px;
|
||||
}
|
||||
QMessageBox QPushButton {
|
||||
background: rgba(30, 30, 30, 0.6);
|
||||
border: 1px solid rgba(165, 165, 165, 0.7);
|
||||
border-radius: 8px;
|
||||
color: #ffffff;
|
||||
font-family: 'Poppins';
|
||||
padding: 8px 20px;
|
||||
min-width: 80px;
|
||||
}
|
||||
QMessageBox QPushButton:hover {
|
||||
background: #09bec8;
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
"""
|
||||
|
||||
# СТИЛИ ДЛЯ ВКЛАДКИ НАСТРОЕК PORTPROTON
|
||||
# PARAMS_TITLE_STYLE
|
||||
PARAMS_TITLE_STYLE = "color: #232627; font-family: 'Poppins'; font-size: 16px; padding: 10px; background: transparent;"
|
||||
|
||||
PROXY_INPUT_STYLE = """
|
||||
QLineEdit {
|
||||
background: rgba(20, 20, 20, 0.40);
|
||||
border: 0px solid rgba(165, 165, 165, 0.7);
|
||||
border-radius: 10px;
|
||||
height: 34px;
|
||||
padding-left: 12px;
|
||||
color: #ffffff;
|
||||
font-family: 'Poppins';
|
||||
font-size: 16px;
|
||||
}
|
||||
QLineEdit:focus {
|
||||
border: 1px solid rgba(0,122,255,0.25);
|
||||
}
|
||||
QMenu {
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
padding: 5px 10px;
|
||||
background: #c7c7c7;
|
||||
}
|
||||
QMenu::item {
|
||||
padding: 0px 10px;
|
||||
border: 10px solid transparent; /* reserve space for selection border */
|
||||
}
|
||||
QMenu::item:selected {
|
||||
background: 1px solid rgba(255, 255, 255, 0.5);
|
||||
border-radius: 10px;
|
||||
}
|
||||
"""
|
||||
|
||||
SETTINGS_COMBO_STYLE = f"""
|
||||
QComboBox {{
|
||||
background: rgba(20, 20, 20, 0.40);
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
border-radius: 10px;
|
||||
height: 34px;
|
||||
padding-left: 12px;
|
||||
color: #ffffff;
|
||||
font-family: 'Poppins';
|
||||
font-size: 16px;
|
||||
min-width: 120px;
|
||||
combobox-popup: 0;
|
||||
}}
|
||||
QComboBox:on {{
|
||||
background: rgba(20, 20, 20, 0.40);
|
||||
border: 1px solid rgba(165, 165, 165, 0.7);
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
}}
|
||||
QComboBox:hover {{
|
||||
border: 1px solid rgba(165, 165, 165, 0.7);
|
||||
}}
|
||||
QComboBox::drop-down {{
|
||||
subcontrol-origin: padding;
|
||||
subcontrol-position: center right;
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.5);
|
||||
padding: 12px;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}}
|
||||
QComboBox::down-arrow {{
|
||||
image: url({theme_manager.get_icon("down", current_theme_name, as_path=True)});
|
||||
padding: 12px;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}}
|
||||
QComboBox::down-arrow:on {{
|
||||
image: url({theme_manager.get_icon("up", current_theme_name, as_path=True)});
|
||||
padding: 12px;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}}
|
||||
QComboBox QAbstractItemView {{
|
||||
outline: none;
|
||||
border: 1px solid rgba(165, 165, 165, 0.7);
|
||||
border-top-style: none;
|
||||
}}
|
||||
QListView {{
|
||||
background: #ffffff;
|
||||
}}
|
||||
QListView::item {{
|
||||
padding: 7px 7px 7px 12px;
|
||||
border-radius: 0px;
|
||||
color: #232627;
|
||||
}}
|
||||
QListView::item:hover {{
|
||||
background: rgba(0,122,255,0.25);
|
||||
}}
|
||||
QListView::item:selected {{
|
||||
background: rgba(0,122,255,0.25);
|
||||
}}
|
||||
"""
|
||||
|
||||
class FileExplorerStyles:
|
||||
WINDOW_STYLE = """
|
||||
QDialog {
|
||||
background-color: #2d2d2d;
|
||||
color: #ffffff;
|
||||
font-family: "Arial";
|
||||
font-size: 14px;
|
||||
}
|
||||
"""
|
||||
|
||||
PATH_LABEL_STYLE = """
|
||||
QLabel {
|
||||
color: #3daee9;
|
||||
font-size: 16px;
|
||||
padding: 5px;
|
||||
}
|
||||
"""
|
||||
|
||||
LIST_STYLE = """
|
||||
QListWidget {
|
||||
font-size: 16px;
|
||||
background-color: #353535;
|
||||
color: #eee;
|
||||
border: 1px solid #444;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QListWidget::item {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
QListWidget::item:selected {
|
||||
background-color: #3daee9;
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
}
|
||||
"""
|
||||
|
||||
BUTTON_STYLE = """
|
||||
QPushButton {
|
||||
background-color: #3daee9;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #2c9fd8;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #1a8fc7;
|
||||
}
|
||||
"""
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
@@ -27,6 +27,10 @@ color_g = "rgba(0, 0, 0, 0)"
|
||||
color_h = "transparent"
|
||||
|
||||
GAME_CARD_ANIMATION = {
|
||||
# Тип анимации при входе и выходе на детальную страницу
|
||||
# Возможные значения: "fade", "slide_left", "slide_right", "slide_up", "slide_down", "bounce"
|
||||
"detail_page_animation_type": "fade",
|
||||
|
||||
# Ширина обводки карточки в состоянии покоя (без наведения или фокуса).
|
||||
# Влияет на толщину рамки вокруг карточки, когда она не выделена.
|
||||
# Значение в пикселях.
|
||||
@@ -93,7 +97,33 @@ GAME_CARD_ANIMATION = {
|
||||
{"position": 0.33, "color": "#FF5733"}, # Цвет на 33% (оранжевый)
|
||||
{"position": 0.66, "color": "#9B59B6"}, # Цвет на 66% (пурпурный)
|
||||
{"position": 1, "color": "#00fff5"} # Конечный цвет (возвращение к циану)
|
||||
]
|
||||
],
|
||||
|
||||
# Длительность анимации fade при входе на детальную страницу
|
||||
"detail_page_fade_duration": 350,
|
||||
|
||||
# Длительность анимации slide при входе на детальную страницу
|
||||
"detail_page_slide_duration": 500,
|
||||
|
||||
# Длительность анимации bounce при входе на детальную страницу
|
||||
"detail_page_bounce_duration": 400,
|
||||
|
||||
# Длительность анимации fade при выходе из детальной страницы
|
||||
"detail_page_fade_duration_exit": 350,
|
||||
|
||||
# Длительность анимации slide при выходе из детальной страницы
|
||||
"detail_page_slide_duration_exit": 500,
|
||||
|
||||
# Длительность анимации bounce при выходе из детальной страницы
|
||||
"detail_page_bounce_duration_exit": 400,
|
||||
|
||||
# Тип кривой сглаживания для анимации при входе на детальную страницу
|
||||
# Применяется к slide и bounce анимациям
|
||||
"detail_page_easing_curve": "OutCubic",
|
||||
|
||||
# Тип кривой сглаживания для анимации при выходе из детальной страницы
|
||||
# Применяется к slide и bounce анимациям
|
||||
"detail_page_easing_curve_exit": "InCubic"
|
||||
}
|
||||
|
||||
CONTEXT_MENU_STYLE = f"""
|
||||
|
@@ -1,49 +0,0 @@
|
||||
from PySide6.QtGui import QAction, QIcon
|
||||
from PySide6.QtWidgets import QSystemTrayIcon, QMenu
|
||||
from portprotonqt.theme_manager import ThemeManager
|
||||
from typing import cast
|
||||
import portprotonqt.themes.standart.styles as default_styles
|
||||
from portprotonqt.config_utils import read_theme_from_config
|
||||
|
||||
class SystemTray:
|
||||
def __init__(self, app, theme=None):
|
||||
self.app = app
|
||||
self.theme_manager = ThemeManager()
|
||||
self.theme = theme if theme is not None else default_styles
|
||||
self.current_theme_name = read_theme_from_config()
|
||||
self.tray = QSystemTrayIcon()
|
||||
self.tray.setIcon(cast(QIcon, self.theme_manager.get_icon("ppqt-tray", self.current_theme_name)))
|
||||
self.tray.setToolTip("PortProtonQt")
|
||||
self.tray.setVisible(True)
|
||||
|
||||
# Создаём меню
|
||||
self.menu = QMenu()
|
||||
|
||||
self.hide_action = QAction("Скрыть окно")
|
||||
self.menu.addAction(self.hide_action)
|
||||
|
||||
self.show_action = QAction("Показать окно")
|
||||
self.menu.addAction(self.show_action)
|
||||
|
||||
self.quit_action = QAction("Выход")
|
||||
self.quit_action.triggered.connect(app.quit)
|
||||
self.menu.addAction(self.quit_action)
|
||||
|
||||
self.tray.setContextMenu(self.menu)
|
||||
|
||||
def hide_tray(self):
|
||||
"""Скрыть иконку трея"""
|
||||
if self.tray:
|
||||
self.tray.setVisible(False)
|
||||
if self.menu:
|
||||
self.menu.deleteLater()
|
||||
self.menu = None
|
||||
|
||||
def cleanup(self):
|
||||
"""Очистка ресурсов трея"""
|
||||
if self.tray:
|
||||
self.tray.setVisible(False)
|
||||
self.tray = None
|
||||
if self.menu:
|
||||
self.menu.deleteLater()
|
||||
self.menu = None
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "portprotonqt"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
description = "A project to rewrite PortProton (PortWINE) using PySide"
|
||||
readme = "README.md"
|
||||
license = { text = "GPL-3.0" }
|
||||
@@ -27,17 +27,19 @@ classifiers = [
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"babel>=2.17.0",
|
||||
"evdev>=1.9.1",
|
||||
"icoextract>=0.1.6",
|
||||
"beautifulsoup4>=4.13.4",
|
||||
"evdev>=1.9.2",
|
||||
"icoextract>=0.2.0",
|
||||
"numpy>=2.2.4",
|
||||
"orjson>=3.10.16",
|
||||
"pillow>=11.2.1",
|
||||
"orjson>=3.11.2",
|
||||
"pillow>=11.3.0",
|
||||
"psutil>=7.0.0",
|
||||
"pyside6>=6.9.0",
|
||||
"pyside6>=6.9.1",
|
||||
"pyudev>=0.24.3",
|
||||
"requests>=2.32.3",
|
||||
"requests>=2.32.4",
|
||||
"tqdm>=4.67.1",
|
||||
"vdf>=3.4",
|
||||
"websocket-client>=1.8.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
@@ -101,7 +103,7 @@ ignore = [
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pre-commit>=4.2.0",
|
||||
"pre-commit>=4.3.0",
|
||||
"pyaspeller>=2.0.2",
|
||||
"pyright>=1.1.400",
|
||||
"pyright>=1.1.403",
|
||||
]
|
||||
|
@@ -15,12 +15,23 @@
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"matchFileNames": [".gitea/workflows/**.yaml", ".gitea/workflows/**.yml"],
|
||||
"matchFileNames": [".python-version"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"matchFileNames": [".python-version"],
|
||||
"matchManagers": ["github-actions", "pre-commit"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"matchManagers": ["pep621"],
|
||||
"rangeStrategy": "bump",
|
||||
"versioning": "pep440",
|
||||
"groupName": "Python dependencies"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["numpy", "setuptools"],
|
||||
"enabled": false,
|
||||
"description": "Disabled due to Python 3.10 incompatibility with numpy>=2.3.2 (requires Python>=3.11)"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
363
uv.lock
generated
@@ -15,6 +15,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "beautifulsoup4"
|
||||
version = "4.13.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "soupsieve" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.6.15"
|
||||
@@ -291,68 +304,79 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "orjson"
|
||||
version = "3.10.18"
|
||||
version = "3.11.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/81/0b/fea456a3ffe74e70ba30e01ec183a9b26bec4d497f61dcfce1b601059c60/orjson-3.10.18.tar.gz", hash = "sha256:e8da3947d92123eda795b68228cafe2724815621fe35e8e320a9e9593a4bcd53", size = 5422810 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/1d/5e0ae38788bdf0721326695e65fdf41405ed535f633eb0df0f06f57552fa/orjson-3.11.2.tar.gz", hash = "sha256:91bdcf5e69a8fd8e8bdb3de32b31ff01d2bd60c1e8d5fe7d5afabdcf19920309", size = 5470739 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/27/16/2ceb9fb7bc2b11b1e4a3ea27794256e93dee2309ebe297fd131a778cd150/orjson-3.10.18-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a45e5d68066b408e4bc383b6e4ef05e717c65219a9e1390abc6155a520cac402", size = 248927 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/e1/d3c0a2bba5b9906badd121da449295062b289236c39c3a7801f92c4682b0/orjson-3.10.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be3b9b143e8b9db05368b13b04c84d37544ec85bb97237b3a923f076265ec89c", size = 136995 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/51/698dd65e94f153ee5ecb2586c89702c9e9d12f165a63e74eb9ea1299f4e1/orjson-3.10.18-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9b0aa09745e2c9b3bf779b096fa71d1cc2d801a604ef6dd79c8b1bfef52b2f92", size = 132893 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/e5/155ce5a2c43a85e790fcf8b985400138ce5369f24ee6770378ee6b691036/orjson-3.10.18-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53a245c104d2792e65c8d225158f2b8262749ffe64bc7755b00024757d957a13", size = 137017 },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/bb/6141ec3beac3125c0b07375aee01b5124989907d61c72c7636136e4bd03e/orjson-3.10.18-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9495ab2611b7f8a0a8a505bcb0f0cbdb5469caafe17b0e404c3c746f9900469", size = 138290 },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/36/6961eca0b66b7809d33c4ca58c6bd4c23a1b914fb23aba2fa2883f791434/orjson-3.10.18-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73be1cbcebadeabdbc468f82b087df435843c809cd079a565fb16f0f3b23238f", size = 142828 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/2f/0c646d5fd689d3be94f4d83fa9435a6c4322c9b8533edbb3cd4bc8c5f69a/orjson-3.10.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe8936ee2679e38903df158037a2f1c108129dee218975122e37847fb1d4ac68", size = 132806 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/af/65907b40c74ef4c3674ef2bcfa311c695eb934710459841b3c2da212215c/orjson-3.10.18-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7115fcbc8525c74e4c2b608129bef740198e9a120ae46184dac7683191042056", size = 135005 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/d1/68bd20ac6a32cd1f1b10d23e7cc58ee1e730e80624e3031d77067d7150fc/orjson-3.10.18-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:771474ad34c66bc4d1c01f645f150048030694ea5b2709b87d3bda273ffe505d", size = 413418 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/31/c701ec0bcc3e80e5cb6e319c628ef7b768aaa24b0f3b4c599df2eaacfa24/orjson-3.10.18-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7c14047dbbea52886dd87169f21939af5d55143dad22d10db6a7514f058156a8", size = 153288 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/31/5e1aa99a10893a43cfc58009f9da840990cc8a9ebb75aa452210ba18587e/orjson-3.10.18-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:641481b73baec8db14fdf58f8967e52dc8bda1f2aba3aa5f5c1b07ed6df50b7f", size = 137181 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/8c/daba0ac1b8690011d9242a0f37235f7d17df6d0ad941021048523b76674e/orjson-3.10.18-cp310-cp310-win32.whl", hash = "sha256:607eb3ae0909d47280c1fc657c4284c34b785bae371d007595633f4b1a2bbe06", size = 142694 },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/62/8b687724143286b63e1d0fab3ad4214d54566d80b0ba9d67c26aaf28a2f8/orjson-3.10.18-cp310-cp310-win_amd64.whl", hash = "sha256:8770432524ce0eca50b7efc2a9a5f486ee0113a5fbb4231526d414e6254eba92", size = 134600 },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c7/c54a948ce9a4278794f669a353551ce7db4ffb656c69a6e1f2264d563e50/orjson-3.10.18-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e0a183ac3b8e40471e8d843105da6fbe7c070faab023be3b08188ee3f85719b8", size = 248929 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/60/a9c674ef1dd8ab22b5b10f9300e7e70444d4e3cda4b8258d6c2488c32143/orjson-3.10.18-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:5ef7c164d9174362f85238d0cd4afdeeb89d9e523e4651add6a5d458d6f7d42d", size = 133364 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/4e/f7d1bdd983082216e414e6d7ef897b0c2957f99c545826c06f371d52337e/orjson-3.10.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd14c5d99cdc7bf93f22b12ec3b294931518aa019e2a147e8aa2f31fd3240f7", size = 136995 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/89/46b9181ba0ea251c9243b0c8ce29ff7c9796fa943806a9c8b02592fce8ea/orjson-3.10.18-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b672502323b6cd133c4af6b79e3bea36bad2d16bca6c1f645903fce83909a7a", size = 132894 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/dd/7bce6fcc5b8c21aef59ba3c67f2166f0a1a9b0317dcca4a9d5bd7934ecfd/orjson-3.10.18-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51f8c63be6e070ec894c629186b1c0fe798662b8687f3d9fdfa5e401c6bd7679", size = 137016 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/4a/b8aea1c83af805dcd31c1f03c95aabb3e19a016b2a4645dd822c5686e94d/orjson-3.10.18-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9478ade5313d724e0495d167083c6f3be0dd2f1c9c8a38db9a9e912cdaf947", size = 138290 },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/d6/7eb05c85d987b688707f45dcf83c91abc2251e0dd9fb4f7be96514f838b1/orjson-3.10.18-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:187aefa562300a9d382b4b4eb9694806e5848b0cedf52037bb5c228c61bb66d4", size = 142829 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/78/ddd3ee7873f2b5f90f016bc04062713d567435c53ecc8783aab3a4d34915/orjson-3.10.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da552683bc9da222379c7a01779bddd0ad39dd699dd6300abaf43eadee38334", size = 132805 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/09/c8e047f73d2c5d21ead9c180203e111cddeffc0848d5f0f974e346e21c8e/orjson-3.10.18-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e450885f7b47a0231979d9c49b567ed1c4e9f69240804621be87c40bc9d3cf17", size = 135008 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/4b/dccbf5055ef8fb6eda542ab271955fc1f9bf0b941a058490293f8811122b/orjson-3.10.18-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5e3c9cc2ba324187cd06287ca24f65528f16dfc80add48dc99fa6c836bb3137e", size = 413419 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/f3/1eac0c5e2d6d6790bd2025ebfbefcbd37f0d097103d76f9b3f9302af5a17/orjson-3.10.18-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:50ce016233ac4bfd843ac5471e232b865271d7d9d44cf9d33773bcd883ce442b", size = 153292 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/b4/ef0abf64c8f1fabf98791819ab502c2c8c1dc48b786646533a93637d8999/orjson-3.10.18-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b3ceff74a8f7ffde0b2785ca749fc4e80e4315c0fd887561144059fb1c138aa7", size = 137182 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/a3/6ea878e7b4a0dc5c888d0370d7752dcb23f402747d10e2257478d69b5e63/orjson-3.10.18-cp311-cp311-win32.whl", hash = "sha256:fdba703c722bd868c04702cac4cb8c6b8ff137af2623bc0ddb3b3e6a2c8996c1", size = 142695 },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/2a/4048700a3233d562f0e90d5572a849baa18ae4e5ce4c3ba6247e4ece57b0/orjson-3.10.18-cp311-cp311-win_amd64.whl", hash = "sha256:c28082933c71ff4bc6ccc82a454a2bffcef6e1d7379756ca567c772e4fb3278a", size = 134603 },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/45/10d934535a4993d27e1c84f1810e79ccf8b1b7418cef12151a22fe9bb1e1/orjson-3.10.18-cp311-cp311-win_arm64.whl", hash = "sha256:a6c7c391beaedd3fa63206e5c2b7b554196f14debf1ec9deb54b5d279b1b46f5", size = 131400 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/1a/67236da0916c1a192d5f4ccbe10ec495367a726996ceb7614eaa687112f2/orjson-3.10.18-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:50c15557afb7f6d63bc6d6348e0337a880a04eaa9cd7c9d569bcb4e760a24753", size = 249184 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/bc/c7f1db3b1d094dc0c6c83ed16b161a16c214aaa77f311118a93f647b32dc/orjson-3.10.18-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:356b076f1662c9813d5fa56db7d63ccceef4c271b1fb3dd522aca291375fcf17", size = 133279 },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/84/664657cd14cc11f0d81e80e64766c7ba5c9b7fc1ec304117878cc1b4659c/orjson-3.10.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:559eb40a70a7494cd5beab2d73657262a74a2c59aff2068fdba8f0424ec5b39d", size = 136799 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/bb/f50039c5bb05a7ab024ed43ba25d0319e8722a0ac3babb0807e543349978/orjson-3.10.18-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3c29eb9a81e2fbc6fd7ddcfba3e101ba92eaff455b8d602bf7511088bbc0eae", size = 132791 },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/8c/ee74709fc072c3ee219784173ddfe46f699598a1723d9d49cbc78d66df65/orjson-3.10.18-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6612787e5b0756a171c7d81ba245ef63a3533a637c335aa7fcb8e665f4a0966f", size = 137059 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/37/e6d3109ee004296c80426b5a62b47bcadd96a3deab7443e56507823588c5/orjson-3.10.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ac6bd7be0dcab5b702c9d43d25e70eb456dfd2e119d512447468f6405b4a69c", size = 138359 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/5d/387dafae0e4691857c62bd02839a3bf3fa648eebd26185adfac58d09f207/orjson-3.10.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f72f100cee8dde70100406d5c1abba515a7df926d4ed81e20a9730c062fe9ad", size = 142853 },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/6f/875e8e282105350b9a5341c0222a13419758545ae32ad6e0fcf5f64d76aa/orjson-3.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dca85398d6d093dd41dc0983cbf54ab8e6afd1c547b6b8a311643917fbf4e0c", size = 133131 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/b2/73a1f0b4790dcb1e5a45f058f4f5dcadc8a85d90137b50d6bbc6afd0ae50/orjson-3.10.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22748de2a07fcc8781a70edb887abf801bb6142e6236123ff93d12d92db3d406", size = 134834 },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/f5/7ed133a5525add9c14dbdf17d011dd82206ca6840811d32ac52a35935d19/orjson-3.10.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3a83c9954a4107b9acd10291b7f12a6b29e35e8d43a414799906ea10e75438e6", size = 413368 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/7c/439654221ed9c3324bbac7bdf94cf06a971206b7b62327f11a52544e4982/orjson-3.10.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:303565c67a6c7b1f194c94632a4a39918e067bd6176a48bec697393865ce4f06", size = 153359 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/e7/d58074fa0cc9dd29a8fa2a6c8d5deebdfd82c6cfef72b0e4277c4017563a/orjson-3.10.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:86314fdb5053a2f5a5d881f03fca0219bfdf832912aa88d18676a5175c6916b5", size = 137466 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/4d/fe17581cf81fb70dfcef44e966aa4003360e4194d15a3f38cbffe873333a/orjson-3.10.18-cp312-cp312-win32.whl", hash = "sha256:187ec33bbec58c76dbd4066340067d9ece6e10067bb0cc074a21ae3300caa84e", size = 142683 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/22/469f62d25ab5f0f3aee256ea732e72dc3aab6d73bac777bd6277955bceef/orjson-3.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:f9f94cf6d3f9cd720d641f8399e390e7411487e493962213390d1ae45c7814fc", size = 134754 },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/b0/1040c447fac5b91bc1e9c004b69ee50abb0c1ffd0d24406e1350c58a7fcb/orjson-3.10.18-cp312-cp312-win_arm64.whl", hash = "sha256:3d600be83fe4514944500fa8c2a0a77099025ec6482e8087d7659e891f23058a", size = 131218 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/f0/8aedb6574b68096f3be8f74c0b56d36fd94bcf47e6c7ed47a7bd1474aaa8/orjson-3.10.18-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:69c34b9441b863175cc6a01f2935de994025e773f814412030f269da4f7be147", size = 249087 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/f7/7118f965541aeac6844fcb18d6988e111ac0d349c9b80cda53583e758908/orjson-3.10.18-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1ebeda919725f9dbdb269f59bc94f861afbe2a27dce5608cdba2d92772364d1c", size = 133273 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/d9/839637cc06eaf528dd8127b36004247bf56e064501f68df9ee6fd56a88ee/orjson-3.10.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5adf5f4eed520a4959d29ea80192fa626ab9a20b2ea13f8f6dc58644f6927103", size = 136779 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/6d/f226ecfef31a1f0e7d6bf9a31a0bbaf384c7cbe3fce49cc9c2acc51f902a/orjson-3.10.18-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7592bb48a214e18cd670974f289520f12b7aed1fa0b2e2616b8ed9e069e08595", size = 132811 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/2d/371513d04143c85b681cf8f3bce743656eb5b640cb1f461dad750ac4b4d4/orjson-3.10.18-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f872bef9f042734110642b7a11937440797ace8c87527de25e0c53558b579ccc", size = 137018 },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/cb/a4d37a30507b7a59bdc484e4a3253c8141bf756d4e13fcc1da760a0b00cb/orjson-3.10.18-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0315317601149c244cb3ecef246ef5861a64824ccbcb8018d32c66a60a84ffbc", size = 138368 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/ae/cd10883c48d912d216d541eb3db8b2433415fde67f620afe6f311f5cd2ca/orjson-3.10.18-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0da26957e77e9e55a6c2ce2e7182a36a6f6b180ab7189315cb0995ec362e049", size = 142840 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/4c/2bda09855c6b5f2c055034c9eda1529967b042ff8d81a05005115c4e6772/orjson-3.10.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb70d489bc79b7519e5803e2cc4c72343c9dc1154258adf2f8925d0b60da7c58", size = 133135 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/4a/35971fd809a8896731930a80dfff0b8ff48eeb5d8b57bb4d0d525160017f/orjson-3.10.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e9e86a6af31b92299b00736c89caf63816f70a4001e750bda179e15564d7a034", size = 134810 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/70/0fa9e6310cda98365629182486ff37a1c6578e34c33992df271a476ea1cd/orjson-3.10.18-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:c382a5c0b5931a5fc5405053d36c1ce3fd561694738626c77ae0b1dfc0242ca1", size = 413491 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/cb/990a0e88498babddb74fb97855ae4fbd22a82960e9b06eab5775cac435da/orjson-3.10.18-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8e4b2ae732431127171b875cb2668f883e1234711d3c147ffd69fe5be51a8012", size = 153277 },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/44/473248c3305bf782a384ed50dd8bc2d3cde1543d107138fd99b707480ca1/orjson-3.10.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d808e34ddb24fc29a4d4041dcfafbae13e129c93509b847b14432717d94b44f", size = 137367 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/fd/7f1d3edd4ffcd944a6a40e9f88af2197b619c931ac4d3cfba4798d4d3815/orjson-3.10.18-cp313-cp313-win32.whl", hash = "sha256:ad8eacbb5d904d5591f27dee4031e2c1db43d559edb8f91778efd642d70e6bea", size = 142687 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/03/c75c6ad46be41c16f4cfe0352a2d1450546f3c09ad2c9d341110cd87b025/orjson-3.10.18-cp313-cp313-win_amd64.whl", hash = "sha256:aed411bcb68bf62e85588f2a7e03a6082cc42e5a2796e06e72a962d7c6310b52", size = 134794 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/28/f53038a5a72cc4fd0b56c1eafb4ef64aec9685460d5ac34de98ca78b6e29/orjson-3.10.18-cp313-cp313-win_arm64.whl", hash = "sha256:f54c1385a0e6aba2f15a40d703b858bedad36ded0491e55d35d905b2c34a4cc3", size = 131186 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/7b/7aebe925c6b1c46c8606a960fe1d6b681fccd4aaf3f37cd647c3309d6582/orjson-3.11.2-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d6b8a78c33496230a60dc9487118c284c15ebdf6724386057239641e1eb69761", size = 226896 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/39/c952c9b0d51063e808117dd1e53668a2e4325cc63cfe7df453d853ee8680/orjson-3.11.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc04036eeae11ad4180d1f7b5faddb5dab1dee49ecd147cd431523869514873b", size = 111845 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/dc/90b7f29be38745eeacc30903b693f29fcc1097db0c2a19a71ffb3e9f2a5f/orjson-3.11.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c04325839c5754c253ff301cee8aaed7442d974860a44447bb3be785c411c27", size = 116395 },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/c2/fe84ba63164c22932b8d59b8810e2e58590105293a259e6dd1bfaf3422c9/orjson-3.11.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32769e04cd7fdc4a59854376211145a1bbbc0aea5e9d6c9755d3d3c301d7c0df", size = 118768 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/ce/d9748ec69b1a4c29b8e2bab8233e8c41c583c69f515b373f1fb00247d8c9/orjson-3.11.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff285d14917ea1408a821786e3677c5261fa6095277410409c694b8e7720ae0", size = 120887 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/66/b90fac8e4a76e83f981912d7f9524d402b31f6c1b8bff3e498aa321c326c/orjson-3.11.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2662f908114864b63ff75ffe6ffacf996418dd6cc25e02a72ad4bda81b1ec45a", size = 123650 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/81/56143898d1689c7f915ac67703efb97e8f2f8d5805ce8c2c3fd0f2bb6e3d/orjson-3.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab463cf5d08ad6623a4dac1badd20e88a5eb4b840050c4812c782e3149fe2334", size = 121287 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/de/f9c6d00c127be766a3739d0d85b52a7c941e437d8dd4d573e03e98d0f89c/orjson-3.11.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:64414241bde943cbf3c00d45fcb5223dca6d9210148ba984aae6b5d63294502b", size = 119637 },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/4c/ab70c7627022d395c1b4eb5badf6196b7144e82b46a3a17ed2354f9e592d/orjson-3.11.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:7773e71c0ae8c9660192ff144a3d69df89725325e3d0b6a6bb2c50e5ebaf9b84", size = 392478 },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/91/d890b873b69311db4fae2624c5603c437df9c857fb061e97706dac550a77/orjson-3.11.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:652ca14e283b13ece35bf3a86503c25592f294dbcfc5bb91b20a9c9a62a3d4be", size = 134343 },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/16/1aa248541b4830274a079c4aeb2aa5d1ff17c3f013b1d0d8d16d0848f3de/orjson-3.11.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:26e99e98df8990ecfe3772bbdd7361f602149715c2cbc82e61af89bfad9528a4", size = 123887 },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/e4/7419833c55ac8b5f385d00c02685a260da1f391e900fc5c3e0b797e0d506/orjson-3.11.2-cp310-cp310-win32.whl", hash = "sha256:5814313b3e75a2be7fe6c7958201c16c4560e21a813dbad25920752cecd6ad66", size = 124560 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/f8/27ca7ef3e194c462af32ce1883187f5ec483650c559166f0de59c4c2c5f0/orjson-3.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:dc471ce2225ab4c42ca672f70600d46a8b8e28e8d4e536088c1ccdb1d22b35ce", size = 119700 },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/7d/e295df1ac9920cbb19fb4c1afa800e86f175cb657143aa422337270a4782/orjson-3.11.2-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:888b64ef7eaeeff63f773881929434a5834a6a140a63ad45183d59287f07fc6a", size = 226502 },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/21/ffb0f10ea04caf418fb4e7ad1fda4b9ab3179df9d7a33b69420f191aadd5/orjson-3.11.2-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:83387cc8b26c9fa0ae34d1ea8861a7ae6cff8fb3e346ab53e987d085315a728e", size = 115999 },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/d5/8da1e252ac3353d92e6f754ee0c85027c8a2cda90b6899da2be0df3ef83d/orjson-3.11.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7e35f003692c216d7ee901b6b916b5734d6fc4180fcaa44c52081f974c08e17", size = 111563 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/81/baabc32e52c570b0e4e1044b1bd2ccbec965e0de3ba2c13082255efa2006/orjson-3.11.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4a0a4c29ae90b11d0c00bcc31533854d89f77bde2649ec602f512a7e16e00640", size = 116222 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/b7/da2ad55ad80b49b560dce894c961477d0e76811ee6e614b301de9f2f8728/orjson-3.11.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:585d712b1880f68370108bc5534a257b561672d1592fae54938738fe7f6f1e33", size = 118594 },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/be/014f7eab51449f3c894aa9bbda2707b5340c85650cb7d0db4ec9ae280501/orjson-3.11.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d08e342a7143f8a7c11f1c4033efe81acbd3c98c68ba1b26b96080396019701f", size = 120700 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/ae/c217903a30c51341868e2d8c318c59a8413baa35af54d7845071c8ccd6fe/orjson-3.11.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c0f84fc50398773a702732c87cd622737bf11c0721e6db3041ac7802a686fb", size = 123433 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/c2/b3c346f78b1ff2da310dd300cb0f5d32167f872b4d3bb1ad122c889d97b0/orjson-3.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:140f84e3c8d4c142575898c91e3981000afebf0333df753a90b3435d349a5fe5", size = 121061 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/c8/c97798f6010327ffc75ad21dd6bca11ea2067d1910777e798c2849f1c68f/orjson-3.11.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96304a2b7235e0f3f2d9363ddccdbfb027d27338722fe469fe656832a017602e", size = 119410 },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/fd/df720f7c0e35694617b7f95598b11a2cb0374661d8389703bea17217da53/orjson-3.11.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d7612bb227d5d9582f1f50a60bd55c64618fc22c4a32825d233a4f2771a428a", size = 392294 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/52/0120d18f60ab0fe47531d520372b528a45c9a25dcab500f450374421881c/orjson-3.11.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a134587d18fe493befc2defffef2a8d27cfcada5696cb7234de54a21903ae89a", size = 134134 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/10/1f967671966598366de42f07e92b0fc694ffc66eafa4b74131aeca84915f/orjson-3.11.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0b84455e60c4bc12c1e4cbaa5cfc1acdc7775a9da9cec040e17232f4b05458bd", size = 123745 },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/eb/76081238671461cfd0f47e0c24f408ffa66184237d56ef18c33e86abb612/orjson-3.11.2-cp311-cp311-win32.whl", hash = "sha256:f0660efeac223f0731a70884e6914a5f04d613b5ae500744c43f7bf7b78f00f9", size = 124393 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/76/cc598c1811ba9ba935171267b02e377fc9177489efce525d478a2999d9cc/orjson-3.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:955811c8405251d9e09cbe8606ad8fdef49a451bcf5520095a5ed38c669223d8", size = 119561 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/17/c48011750f0489006f7617b0a3cebc8230f36d11a34e7e9aca2085f07792/orjson-3.11.2-cp311-cp311-win_arm64.whl", hash = "sha256:2e4d423a6f838552e3a6d9ec734b729f61f88b1124fd697eab82805ea1a2a97d", size = 114186 },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/02/46054ebe7996a8adee9640dcad7d39d76c2000dc0377efa38e55dc5cbf78/orjson-3.11.2-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:901d80d349d8452162b3aa1afb82cec5bee79a10550660bc21311cc61a4c5486", size = 226528 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/c6/6b6f0b4d8aea1137436546b990f71be2cd8bd870aa2f5aa14dba0fcc95dc/orjson-3.11.2-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:cf3bd3967a360e87ee14ed82cb258b7f18c710dacf3822fb0042a14313a673a1", size = 115931 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/05/4205cc97c30e82a293dd0d149b1a89b138ebe76afeca66fc129fa2aa4e6a/orjson-3.11.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26693dde66910078229a943e80eeb99fdce6cd2c26277dc80ead9f3ab97d2131", size = 111382 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/c7/b8a951a93caa821f9272a7c917115d825ae2e4e8768f5ddf37968ec9de01/orjson-3.11.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad4c8acb50a28211c33fc7ef85ddf5cb18d4636a5205fd3fa2dce0411a0e30c", size = 116271 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/03/1006c7f8782d5327439e26d9b0ec66500ea7b679d4bbb6b891d2834ab3ee/orjson-3.11.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:994181e7f1725bb5f2d481d7d228738e0743b16bf319ca85c29369c65913df14", size = 119086 },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/61/57d22bc31f36a93878a6f772aea76b2184102c6993dea897656a66d18c74/orjson-3.11.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dbb79a0476393c07656b69c8e763c3cc925fa8e1d9e9b7d1f626901bb5025448", size = 120724 },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/a9/4550e96b4c490c83aea697d5347b8f7eb188152cd7b5a38001055ca5b379/orjson-3.11.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:191ed27a1dddb305083d8716af413d7219f40ec1d4c9b0e977453b4db0d6fb6c", size = 123577 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/86/09b8cb3ebd513d708ef0c92d36ac3eebda814c65c72137b0a82d6d688fc4/orjson-3.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0afb89f16f07220183fd00f5f297328ed0a68d8722ad1b0c8dcd95b12bc82804", size = 121195 },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/68/7b40b39ac2c1c644d4644e706d0de6c9999764341cd85f2a9393cb387661/orjson-3.11.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ab6e6b4e93b1573a026b6ec16fca9541354dd58e514b62c558b58554ae04307", size = 119234 },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/7c/bb6e7267cd80c19023d44d8cbc4ea4ed5429fcd4a7eb9950f50305697a28/orjson-3.11.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9cb23527efb61fb75527df55d20ee47989c4ee34e01a9c98ee9ede232abf6219", size = 392250 },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/f2/6730ace05583dbca7c1b406d59f4266e48cd0d360566e71482420fb849fc/orjson-3.11.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a4dd1268e4035af21b8a09e4adf2e61f87ee7bf63b86d7bb0a237ac03fad5b45", size = 134572 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/0f/7d3e03a30d5aac0432882b539a65b8c02cb6dd4221ddb893babf09c424cc/orjson-3.11.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff8b155b145eaf5a9d94d2c476fbe18d6021de93cf36c2ae2c8c5b775763f14e", size = 123869 },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/80/1513265eba6d4a960f078f4b1d2bff94a571ab2d28c6f9835e03dfc65cc6/orjson-3.11.2-cp312-cp312-win32.whl", hash = "sha256:ae3bb10279d57872f9aba68c9931aa71ed3b295fa880f25e68da79e79453f46e", size = 124430 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/61/eadf057b68a332351eeb3d89a4cc538d14f31cd8b5ec1b31a280426ccca2/orjson-3.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:d026e1967239ec11a2559b4146a61d13914504b396f74510a1c4d6b19dfd8732", size = 119598 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/3f/7f4b783402143d965ab7e9a2fc116fdb887fe53bdce7d3523271cd106098/orjson-3.11.2-cp312-cp312-win_arm64.whl", hash = "sha256:59f8d5ad08602711af9589375be98477d70e1d102645430b5a7985fdbf613b36", size = 114052 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/f3/0dd6b4750eb556ae4e2c6a9cb3e219ec642e9c6d95f8ebe5dc9020c67204/orjson-3.11.2-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a079fdba7062ab396380eeedb589afb81dc6683f07f528a03b6f7aae420a0219", size = 226419 },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/d5/e67f36277f78f2af8a4690e0c54da6b34169812f807fd1b4bfc4dbcf9558/orjson-3.11.2-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:6a5f62ebbc530bb8bb4b1ead103647b395ba523559149b91a6c545f7cd4110ad", size = 115803 },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/37/ff8bc86e0dacc48f07c2b6e20852f230bf4435611bab65e3feae2b61f0ae/orjson-3.11.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7df6c7b8b0931feb3420b72838c3e2ba98c228f7aa60d461bc050cf4ca5f7b2", size = 111337 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/25/37d4d3e8079ea9784ea1625029988e7f4594ce50d4738b0c1e2bf4a9e201/orjson-3.11.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6f59dfea7da1fced6e782bb3699718088b1036cb361f36c6e4dd843c5111aefe", size = 116222 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/32/a63fd9c07fce3b4193dcc1afced5dd4b0f3a24e27556604e9482b32189c9/orjson-3.11.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edf49146520fef308c31aa4c45b9925fd9c7584645caca7c0c4217d7900214ae", size = 119020 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/b6/400792b8adc3079a6b5d649264a3224d6342436d9fac9a0ed4abc9dc4596/orjson-3.11.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50995bbeb5d41a32ad15e023305807f561ac5dcd9bd41a12c8d8d1d2c83e44e6", size = 120721 },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/f3/31ab8f8c699eb9e65af8907889a0b7fef74c1d2b23832719a35da7bb0c58/orjson-3.11.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cc42960515076eb639b705f105712b658c525863d89a1704d984b929b0577d1", size = 123574 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/a6/ce4287c412dff81878f38d06d2c80845709c60012ca8daf861cb064b4574/orjson-3.11.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c56777cab2a7b2a8ea687fedafb84b3d7fdafae382165c31a2adf88634c432fa", size = 121225 },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/b0/7a881b2aef4fed0287d2a4fbb029d01ed84fa52b4a68da82bdee5e50598e/orjson-3.11.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:07349e88025b9b5c783077bf7a9f401ffbfb07fd20e86ec6fc5b7432c28c2c5e", size = 119201 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/98/a325726b37f7512ed6338e5e65035c3c6505f4e628b09a5daf0419f054ea/orjson-3.11.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:45841fbb79c96441a8c58aa29ffef570c5df9af91f0f7a9572e5505e12412f15", size = 392193 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/4f/a7194f98b0ce1d28190e0c4caa6d091a3fc8d0107ad2209f75c8ba398984/orjson-3.11.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:13d8d8db6cd8d89d4d4e0f4161acbbb373a4d2a4929e862d1d2119de4aa324ac", size = 134548 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/5e/b84caa2986c3f472dc56343ddb0167797a708a8d5c3be043e1e2677b55df/orjson-3.11.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51da1ee2178ed09c00d09c1b953e45846bbc16b6420965eb7a913ba209f606d8", size = 123798 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/5b/e398449080ce6b4c8fcadad57e51fa16f65768e1b142ba90b23ac5d10801/orjson-3.11.2-cp313-cp313-win32.whl", hash = "sha256:51dc033df2e4a4c91c0ba4f43247de99b3cbf42ee7a42ee2b2b2f76c8b2f2cb5", size = 124402 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/66/429e4608e124debfc4790bfc37131f6958e59510ba3b542d5fc163be8e5f/orjson-3.11.2-cp313-cp313-win_amd64.whl", hash = "sha256:29d91d74942b7436f29b5d1ed9bcfc3f6ef2d4f7c4997616509004679936650d", size = 119498 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/04/f8b5f317cce7ad3580a9ad12d7e2df0714dfa8a83328ecddd367af802f5b/orjson-3.11.2-cp313-cp313-win_arm64.whl", hash = "sha256:4ca4fb5ac21cd1e48028d4f708b1bb13e39c42d45614befd2ead004a8bba8535", size = 114051 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/83/2c363022b26c3c25b3708051a19d12f3374739bb81323f05b284392080c0/orjson-3.11.2-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3dcba7101ea6a8d4ef060746c0f2e7aa8e2453a1012083e1ecce9726d7554cb7", size = 226406 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/a7/aa3c973de0b33fc93b4bd71691665ffdfeae589ea9d0625584ab10a7d0f5/orjson-3.11.2-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:15d17bdb76a142e1f55d91913e012e6e6769659daa6bfef3ef93f11083137e81", size = 115788 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/f2/e45f233dfd09fdbb052ec46352363dca3906618e1a2b264959c18f809d0b/orjson-3.11.2-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:53c9e81768c69d4b66b8876ec3c8e431c6e13477186d0db1089d82622bccd19f", size = 111318 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/23/cf5a73c4da6987204cbbf93167f353ff0c5013f7c5e5ef845d4663a366da/orjson-3.11.2-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:d4f13af59a7b84c1ca6b8a7ab70d608f61f7c44f9740cd42409e6ae7b6c8d8b7", size = 121231 },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/1d/47468a398ae68a60cc21e599144e786e035bb12829cb587299ecebc088f1/orjson-3.11.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bde64aa469b5ee46cc960ed241fae3721d6a8801dacb2ca3466547a2535951e4", size = 119204 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/d9/f99433d89b288b5bc8836bffb32a643f805e673cf840ef8bab6e73ced0d1/orjson-3.11.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b5ca86300aeb383c8fa759566aca065878d3d98c3389d769b43f0a2e84d52c5f", size = 392237 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/dc/1b9d80d40cebef603325623405136a29fb7d08c877a728c0943dd066c29a/orjson-3.11.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:24e32a558ebed73a6a71c8f1cbc163a7dd5132da5270ff3d8eeb727f4b6d1bc7", size = 134578 },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/b3/72e7a4c5b6485ef4e83ef6aba7f1dd041002bad3eb5d1d106ca5b0fc02c6/orjson-3.11.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e36319a5d15b97e4344110517450396845cc6789aed712b1fbf83c1bd95792f6", size = 123799 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/3e/a3d76b392e7acf9b34dc277171aad85efd6accc75089bb35b4c614990ea9/orjson-3.11.2-cp314-cp314-win32.whl", hash = "sha256:40193ada63fab25e35703454d65b6afc71dbc65f20041cb46c6d91709141ef7f", size = 124461 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/e3/75c6a596ff8df9e4a5894813ff56695f0a218e6ea99420b4a645c4f7795d/orjson-3.11.2-cp314-cp314-win_amd64.whl", hash = "sha256:7c8ac5f6b682d3494217085cf04dadae66efee45349ad4ee2a1da3c97e2305a8", size = 119494 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/3d/9e74742fc261c5ca473c96bb3344d03995869e1dc6402772c60afb97736a/orjson-3.11.2-cp314-cp314-win_arm64.whl", hash = "sha256:21cf261e8e79284242e4cb1e5924df16ae28255184aafeff19be1405f6d33f67", size = 114046 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -366,79 +390,104 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "11.2.1"
|
||||
version = "11.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/8b/b158ad57ed44d3cc54db8d68ad7c0a58b8fc0e4c7a3f995f9d62d5b464a1/pillow-11.2.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047", size = 3198442 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/f8/bb5d956142f86c2d6cc36704943fa761f2d2e4c48b7436fd0a85c20f1713/pillow-11.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95", size = 3030553 },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/7f/0e413bb3e2aa797b9ca2c5c38cb2e2e45d88654e5b12da91ad446964cfae/pillow-11.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61", size = 4405503 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/b4/cc647f4d13f3eb837d3065824aa58b9bcf10821f029dc79955ee43f793bd/pillow-11.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1", size = 4490648 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/6f/240b772a3b35cdd7384166461567aa6713799b4e78d180c555bd284844ea/pillow-11.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c", size = 4508937 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/5e/7ca9c815ade5fdca18853db86d812f2f188212792780208bdb37a0a6aef4/pillow-11.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d", size = 4599802 },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/81/c3d9d38ce0c4878a77245d4cf2c46d45a4ad0f93000227910a46caff52f3/pillow-11.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97", size = 4576717 },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/49/52b719b89ac7da3185b8d29c94d0e6aec8140059e3d8adcaa46da3751180/pillow-11.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579", size = 4654874 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/0b/ede75063ba6023798267023dc0d0401f13695d228194d2242d5a7ba2f964/pillow-11.2.1-cp310-cp310-win32.whl", hash = "sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d", size = 2331717 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/3c/9831da3edea527c2ed9a09f31a2c04e77cd705847f13b69ca60269eec370/pillow-11.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad", size = 2676204 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/97/1f66ff8a1503d8cbfc5bae4dc99d54c6ec1e22ad2b946241365320caabc2/pillow-11.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2", size = 2414767 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/08/3fbf4b98924c73037a8e8b4c2c774784805e0fb4ebca6c5bb60795c40125/pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70", size = 3198450 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/92/6505b1af3d2849d5e714fc75ba9e69b7255c05ee42383a35a4d58f576b16/pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf", size = 3030550 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/8c/ac2f99d2a70ff966bc7eb13dacacfaab57c0549b2ffb351b6537c7840b12/pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7", size = 4415018 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/e3/0a58b5d838687f40891fff9cbaf8669f90c96b64dc8f91f87894413856c6/pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8", size = 4498006 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/f5/6ba14718135f08fbfa33308efe027dd02b781d3f1d5c471444a395933aac/pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600", size = 4517773 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/f2/805ad600fc59ebe4f1ba6129cd3a75fb0da126975c8579b8f57abeb61e80/pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788", size = 4607069 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/6b/4ef8a288b4bb2e0180cba13ca0a519fa27aa982875882392b65131401099/pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e", size = 4583460 },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/ae/f29c705a09cbc9e2a456590816e5c234382ae5d32584f451c3eb41a62062/pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e", size = 4661304 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/1a/c8217b6f2f73794a5e219fbad087701f412337ae6dbb956db37d69a9bc43/pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6", size = 2331809 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/72/25a8f40170dc262e86e90f37cb72cb3de5e307f75bf4b02535a61afcd519/pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193", size = 2676338 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/9e/76825e39efee61efea258b479391ca77d64dbd9e5804e4ad0fa453b4ba55/pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7", size = 2414918 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/40/052610b15a1b8961f52537cc8326ca6a881408bc2bdad0d852edeb6ed33b/pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f", size = 3190185 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/7e/b86dbd35a5f938632093dc40d1682874c33dcfe832558fc80ca56bfcb774/pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b", size = 3030306 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/5c/467a161f9ed53e5eab51a42923c33051bf8d1a2af4626ac04f5166e58e0c/pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d", size = 4416121 },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/73/972b7742e38ae0e2ac76ab137ca6005dcf877480da0d9d61d93b613065b4/pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4", size = 4501707 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/3a/427e4cb0b9e177efbc1a84798ed20498c4f233abde003c06d2650a6d60cb/pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d", size = 4522921 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/7c/d8b1330458e4d2f3f45d9508796d7caf0c0d3764c00c823d10f6f1a3b76d/pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4", size = 4612523 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/2f/65738384e0b1acf451de5a573d8153fe84103772d139e1e0bdf1596be2ea/pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443", size = 4587836 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/c5/e795c9f2ddf3debb2dedd0df889f2fe4b053308bb59a3cc02a0cd144d641/pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c", size = 4669390 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/ae/ca0099a3995976a9fce2f423166f7bff9b12244afdc7520f6ed38911539a/pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3", size = 2332309 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/18/24bff2ad716257fc03da964c5e8f05d9790a779a8895d6566e493ccf0189/pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941", size = 2676768 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087 },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674 },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008 },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420 },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388 },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049 },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895 },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/49/c8c21e4255b4f4a2c0c68ac18125d7f5460b109acc6dfdef1a24f9b960ef/pillow-11.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156", size = 3181727 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/f1/f7255c0838f8c1ef6d55b625cfb286835c17e8136ce4351c5577d02c443b/pillow-11.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772", size = 2999833 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/57/9968114457bd131063da98d87790d080366218f64fa2943b65ac6739abb3/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363", size = 3437472 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/1b/e35d8a158e21372ecc48aac9c453518cfe23907bb82f950d6e1c72811eb0/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0", size = 3459976 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/da/2c11d03b765efff0ccc473f1c4186dc2770110464f2177efaed9cf6fae01/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01", size = 3527133 },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/1a/4e85bd7cadf78412c2a3069249a09c32ef3323650fd3005c97cca7aa21df/pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193", size = 3571555 },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/03/239939915216de1e95e0ce2334bf17a7870ae185eb390fab6d706aadbfc0/pillow-11.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013", size = 2674713 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/ad/2613c04633c7257d9481ab21d6b5364b59fc5d75faafd7cb8693523945a3/pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed", size = 3181734 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/fd/dcdda4471ed667de57bb5405bb42d751e6cfdd4011a12c248b455c778e03/pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c", size = 2999841 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/89/8a2536e95e77432833f0db6fd72a8d310c8e4272a04461fb833eb021bf94/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd", size = 3437470 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/8f/abd47b73c60712f88e9eda32baced7bfc3e9bd6a7619bb64b93acff28c3e/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076", size = 3460013 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/20/5c0a0aa83b213b7a07ec01e71a3d6ea2cf4ad1d2c686cc0168173b6089e7/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b", size = 3527165 },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/0e/2abab98a72202d91146abc839e10c14f7cf36166f12838ea0c4db3ca6ecb/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f", size = 3571586 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/2c/5e05f58658cf49b6667762cca03d6e7d85cededde2caf2ab37b81f80e574/pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044", size = 2674751 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087 },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899 },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273 },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079 },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652 },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236 },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324 },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574 },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407 },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841 },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110 },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132 },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520 },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246 },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694 },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500 },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207 },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166 },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248 },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505 },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -452,10 +501,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "portprotonqt"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "babel" },
|
||||
{ name = "beautifulsoup4" },
|
||||
{ name = "evdev" },
|
||||
{ name = "icoextract" },
|
||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
@@ -468,6 +518,7 @@ dependencies = [
|
||||
{ name = "requests" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "vdf" },
|
||||
{ name = "websocket-client" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
@@ -480,29 +531,31 @@ dev = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "babel", specifier = ">=2.17.0" },
|
||||
{ name = "evdev", specifier = ">=1.9.1" },
|
||||
{ name = "icoextract", specifier = ">=0.1.6" },
|
||||
{ name = "beautifulsoup4", specifier = ">=4.13.4" },
|
||||
{ name = "evdev", specifier = ">=1.9.2" },
|
||||
{ name = "icoextract", specifier = ">=0.2.0" },
|
||||
{ name = "numpy", specifier = ">=2.2.4" },
|
||||
{ name = "orjson", specifier = ">=3.10.16" },
|
||||
{ name = "pillow", specifier = ">=11.2.1" },
|
||||
{ name = "orjson", specifier = ">=3.11.2" },
|
||||
{ name = "pillow", specifier = ">=11.3.0" },
|
||||
{ name = "psutil", specifier = ">=7.0.0" },
|
||||
{ name = "pyside6", specifier = ">=6.9.0" },
|
||||
{ name = "pyside6", specifier = ">=6.9.1" },
|
||||
{ name = "pyudev", specifier = ">=0.24.3" },
|
||||
{ name = "requests", specifier = ">=2.32.3" },
|
||||
{ name = "requests", specifier = ">=2.32.4" },
|
||||
{ name = "tqdm", specifier = ">=4.67.1" },
|
||||
{ name = "vdf", specifier = ">=3.4" },
|
||||
{ name = "websocket-client", specifier = ">=1.8.0" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "pre-commit", specifier = ">=4.2.0" },
|
||||
{ name = "pre-commit", specifier = ">=4.3.0" },
|
||||
{ name = "pyaspeller", specifier = ">=2.0.2" },
|
||||
{ name = "pyright", specifier = ">=1.1.400" },
|
||||
{ name = "pyright", specifier = ">=1.1.403" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "4.2.0"
|
||||
version = "4.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cfgv" },
|
||||
@@ -511,9 +564,9 @@ dependencies = [
|
||||
{ name = "pyyaml" },
|
||||
{ name = "virtualenv" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -545,15 +598,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pyright"
|
||||
version = "1.1.402"
|
||||
version = "1.1.403"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nodeenv" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/04/ce0c132d00e20f2d2fb3b3e7c125264ca8b909e693841210534b1ea1752f/pyright-1.1.402.tar.gz", hash = "sha256:85a33c2d40cd4439c66aa946fd4ce71ab2f3f5b8c22ce36a623f59ac22937683", size = 3888207 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/f6/35f885264ff08c960b23d1542038d8da86971c5d8c955cfab195a4f672d7/pyright-1.1.403.tar.gz", hash = "sha256:3ab69b9f41c67fb5bbb4d7a36243256f0d549ed3608678d381d5f51863921104", size = 3913526 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/37/1a1c62d955e82adae588be8e374c7f77b165b6cb4203f7d581269959abbc/pyright-1.1.402-py3-none-any.whl", hash = "sha256:2c721f11869baac1884e846232800fe021c33f1b4acb3929cff321f7ea4e2982", size = 5624004 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/b6/b04e5c2f41a5ccad74a1a4759da41adb20b4bc9d59a5e08d29ba60084d07/pyright-1.1.403-py3-none-any.whl", hash = "sha256:c0eeca5aa76cbef3fcc271259bbd785753c7ad7bcac99a9162b4c4c7daed23b3", size = 5684504 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -684,6 +737,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/de/ce/6ccd382fbe1a96926c5514afa6f2c42da3a9a8482e61f8dfc6068a9ca64f/shiboken6-6.9.1-cp39-abi3-win_arm64.whl", hash = "sha256:2a39997ce275ced7853defc89d3a1f19a11c90991ac6eef3435a69bb0b7ff1de", size = 1831623 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "soupsieve"
|
||||
version = "2.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.67.1"
|
||||
@@ -736,3 +798,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "websocket-client"
|
||||
version = "1.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826 },
|
||||
]
|
||||
|