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