mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-30 06:59:13 +00:00
Compare commits
184 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b378a25c8c | |||
| 0c96738530 | |||
| 9875c4ddbc | |||
| a12d9d2bdb | |||
| e266258fc4 | |||
| 725ebd5835 | |||
| 58cd6e9541 | |||
| 5697d98862 | |||
| 6cd4fb5553 | |||
| a3143c11a8 | |||
| d71fb7935b | |||
| 107b9abb3e | |||
| 0cfb41755a | |||
| 2f8b5df007 | |||
| 69e9f998e9 | |||
| a8d55785cf | |||
| d067fe6361 | |||
| c02a78ed6e | |||
| c2f8cba4af | |||
| c36f1985bd | |||
| fc52cf53ee | |||
| dcff7d146b | |||
| 13a38d82fd | |||
| b537968feb | |||
| 983fcf3014 | |||
| 61ad88db77 | |||
| 84577c8708 | |||
| 6bd3951d31 | |||
| 48e79f803a | |||
| 7f421f5bd4 | |||
| d6e71b23ef | |||
| 54aecee64e | |||
| 822b6bcaaa | |||
| 259aabf82f | |||
| 1de8c5915b | |||
| 4f92276147 | |||
| f11e34ab25 | |||
| 23944f7cbf | |||
| e6b1950bcb | |||
| 0c6c7b1b06 | |||
| 9c8db576f5 | |||
| a84a13c76d | |||
| 96cc72f2ca | |||
| 372771a228 | |||
| 675b6d5ef8 | |||
| 97b62ac72b | |||
| 0b1c069448 | |||
| 43b940e216 | |||
| 10e13571e5 | |||
| 14ea7b964f | |||
| 2a22f000c1 | |||
| 63f90d884e | |||
| a1a29616e5 | |||
| 6b06bae250 | |||
| 9a0a52f9e3 | |||
| 6f7dd26d56 | |||
| 28aca03f0d | |||
| f9ff518e6d | |||
| de9d2144a6 | |||
| 11b743a792 | |||
| 637e3c566e | |||
| 89a4a69f6d | |||
| a449181802 | |||
| 4696583680 | |||
| 6d2e9365c0 | |||
| 61cf80f8a2 | |||
| 39e752e4e2 | |||
| 992e2e2ad3 | |||
| c2ebc78836 | |||
| dc1697d049 | |||
| 35f6f041c1 | |||
| 7e3f9edacf | |||
| 22b732bab4 | |||
| 61b984bfa1 | |||
| 867ce63f7b | |||
| ee6122b97d | |||
| 091428f683 | |||
| 9db3f3f20e | |||
| 37d70c31ff | |||
| ee1627a3a1 | |||
| ecd8ef5998 | |||
| 8987076c5f | |||
| ec41c61219 | |||
| 21c7742f9a | |||
| e8add28797 | |||
| f5e902f741 | |||
| 063961d36a | |||
| 96ce599e34 | |||
| c5ea95a787 | |||
| db96121304 | |||
| 40c7c1b515 | |||
| 70104065e2 | |||
| 11a88186ba | |||
| 6522ebf0ca | |||
| 6042ca56b8 | |||
| 5190765ce1 | |||
| 3a38cf8074 | |||
| 4f40b97f53 | |||
| 5fd5b2f08c | |||
| 87d7e15951 | |||
| 98e2d1232c | |||
| 6cb545c78d | |||
| 41c62c3a34 | |||
| 31a6202da9 | |||
| 3565c3c9b4 | |||
| e2ade0d731 | |||
| 06690890fb | |||
| 8b0d1753ef | |||
| 70532c8d0b | |||
| 5e1e035bc2 | |||
| 34d1a34b10 | |||
| cfb6fe69e3 | |||
| 1921e7ec56 | |||
| 05b41b3410 | |||
| 18d89e76fd | |||
| 4a9b45b98e | |||
| 8f299a90dc | |||
| 5a633fdd82 | |||
| 8ce4a7679e | |||
| a0920cf0d0 | |||
| 7565a66d90 | |||
| e4b1efc44d | |||
| 85f4604bfd | |||
| f30309d153 | |||
| f48d774554 | |||
| 268311c482 | |||
| ed0f851d4d | |||
| 2f4e79a40e | |||
| f508d1dfce | |||
| 8dc6f19d2b | |||
| 321242b0d9 | |||
| 949c5e73c4 | |||
| da9217ac4a | |||
| 39f64ca666 | |||
| 2391af09eb | |||
| 0b01df7339 | |||
| c69ca5e821 | |||
| b155734dcf | |||
| 96ae502202 | |||
| b373c3114b | |||
| af6784272c | |||
| df08f611b9 | |||
| de1b24b8bc | |||
| fae09596a7 | |||
| e8f5ce8d5a | |||
| 81726acd51 | |||
| 5582457c58 | |||
| df755063c2 | |||
| 1949e9fcf1 | |||
| 28ca7b5c90 | |||
| feabb582b2 | |||
| 47e9708a20 | |||
| 934b1b35ad | |||
| 88250b3e20 | |||
| d9ba637cd9 | |||
| dcee5ca4e4 | |||
| dffa82a555 | |||
| 63f8cff341 | |||
| 836bcfbc03 | |||
| 64867369f9 | |||
| 315b2f9cda | |||
| 07c8ddc0b2 | |||
| 36c145bb3a | |||
| 19793d71e6 | |||
| 796b8b49f4 | |||
| 1b9d674a81 | |||
| 949244e8e6 | |||
| b7d4d57bc2 | |||
| 458fa6a66c | |||
| 06fccb792f | |||
| 6867feba85 | |||
| 45353c87c2 | |||
| 37b8c5264f | |||
| 5937971014 | |||
| a351c02ac1 | |||
| fc086db5e6 | |||
| ca1332334d | |||
| aebca54eac | |||
| 8fe8062950 | |||
| 11d731cf35 | |||
| bf83eabe6b | |||
| a7c358844c | |||
| e2d708a6af | |||
| 1e1e82ed71 |
@@ -14,24 +14,28 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout 🧰
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up NodeJS 18 💎
|
||||
uses: actions/setup-node@v3
|
||||
- name: Set up NodeJS 20 💎
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- name: Set up Python 3.11.4 🐍
|
||||
uses: actions/setup-python@v4
|
||||
- name: Set up Python 3.11.7 🐍
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11.4"
|
||||
python-version: "3.11.7"
|
||||
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
virtualenvs-create: false
|
||||
|
||||
- name: Install Python dependencies ⬇️
|
||||
working-directory: ./backend
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pyinstaller==5.13.0
|
||||
pip install -r requirements.txt
|
||||
C:\Users\runneradmin\.local\bin\poetry self add "poetry-dynamic-versioning[plugin]"
|
||||
C:\Users\runneradmin\.local\bin\poetry install --no-interaction
|
||||
|
||||
- name: Install JS dependencies ⬇️
|
||||
working-directory: ./frontend
|
||||
@@ -44,16 +48,20 @@ jobs:
|
||||
run: pnpm run build
|
||||
|
||||
- name: Build Python Backend 🛠️
|
||||
run: pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data "./backend/static;/static" --add-data "./backend/locales;/locales" --add-data "./backend/src/legacy;/src/legacy" --add-data "./plugin;/plugin" --hidden-import=logging.handlers --hidden-import=sqlite3 ./backend/main.py
|
||||
working-directory: ./backend
|
||||
run: |
|
||||
C:\Users\runneradmin\.local\bin\poetry dynamic-versioning
|
||||
C:\Users\runneradmin\.local\bin\poetry run pyinstaller pyinstaller.spec
|
||||
|
||||
- name: Build Python Backend (noconsole) 🛠️
|
||||
run: pyinstaller --noconfirm --noconsole --onefile --name "PluginLoader_noconsole" --add-data "./backend/static;/static" --add-data "./backend/locales;/locales" --add-data "./backend/src/legacy;/src/legacy" --add-data "./plugin;/plugin" --hidden-import=logging.handlers --hidden-import=sqlite3 ./backend/main.py
|
||||
|
||||
working-directory: ./backend
|
||||
run: $env:DECKY_NOCONSOLE = 1; C:\Users\runneradmin\.local\bin\poetry run pyinstaller pyinstaller.spec
|
||||
|
||||
- name: Upload package artifact ⬆️
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: PluginLoader Win
|
||||
path: |
|
||||
./dist/PluginLoader.exe
|
||||
./dist/PluginLoader_noconsole.exe
|
||||
./backend/dist/PluginLoader.exe
|
||||
./backend/dist/PluginLoader_noconsole.exe
|
||||
|
||||
|
||||
+25
-225
@@ -3,30 +3,9 @@ name: Builder
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_call:
|
||||
# schedule:
|
||||
# - cron: '0 13 * * *' # run at 1 PM UTC
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release:
|
||||
type: choice
|
||||
description: Release the asset
|
||||
default: 'none'
|
||||
options:
|
||||
- none
|
||||
- prerelease
|
||||
- release
|
||||
bump:
|
||||
type: choice
|
||||
description: Semver to bump
|
||||
default: 'none'
|
||||
options:
|
||||
- none
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -34,47 +13,31 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Print input
|
||||
run : |
|
||||
echo "release: ${{ github.event.inputs.release }}\n"
|
||||
echo "bump: ${{ github.event.inputs.bump }}\n"
|
||||
|
||||
- name: Checkout 🧰
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up NodeJS 18 💎
|
||||
uses: actions/setup-node@v3
|
||||
- name: Set up NodeJS 20 💎
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- name: Set up Python 3.10.6 🐍
|
||||
uses: actions/setup-python@v4
|
||||
- name: Set up Python 3.11.7 🐍
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10.6"
|
||||
python-version: "3.11.7"
|
||||
|
||||
- name: Upgrade SQLite 3 binary version to 3.42.0 🧑💻
|
||||
run: >
|
||||
cd /tmp &&
|
||||
wget "https://www.sqlite.org/2023/sqlite-autoconf-3420000.tar.gz" &&
|
||||
tar -xvzf sqlite-autoconf-3420000.tar.gz &&
|
||||
cd /tmp/sqlite-autoconf-3420000 &&
|
||||
./configure --prefix=/usr --disable-static CFLAGS="-g" CPPFLAGS="$CPPFLAGS -DSQLITE_ENABLE_COLUMN_METADATA=1 \
|
||||
-DSQLITE_ENABLE_UNLOCK_NOTIFY -DSQLITE_ENABLE_DBSTAT_VTAB=1 -DSQLITE_ENABLE_FTS3_TOKENIZER=1 \
|
||||
-DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_SECURE_DELETE -DSQLITE_ENABLE_STMTVTAB -DSQLITE_MAX_VARIABLE_NUMBER=250000 \
|
||||
-DSQLITE_MAX_EXPR_DEPTH=10000 -DSQLITE_ENABLE_MATH_FUNCTIONS" &&
|
||||
make -j$(nproc) &&
|
||||
sudo make install &&
|
||||
sudo cp /usr/lib/libsqlite3.so /usr/lib/x86_64-linux-gnu/ &&
|
||||
sudo cp /usr/lib/libsqlite3.so.0 /usr/lib/x86_64-linux-gnu/ &&
|
||||
sudo cp /usr/lib/libsqlite3.so.0.8.6 /usr/lib/x86_64-linux-gnu/ &&
|
||||
rm -r /tmp/sqlite-autoconf-3420000
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
virtualenvs-create: false
|
||||
|
||||
- name: Install Python dependencies ⬇️
|
||||
working-directory: ./backend
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pyinstaller==5.13.0
|
||||
pip install -r requirements.txt
|
||||
poetry self add "poetry-dynamic-versioning[plugin]"
|
||||
poetry install --no-interaction
|
||||
|
||||
- name: Install JS dependencies ⬇️
|
||||
working-directory: ./frontend
|
||||
@@ -87,183 +50,20 @@ jobs:
|
||||
run: pnpm run build
|
||||
|
||||
- name: Build Python Backend 🛠️
|
||||
run: pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data ./backend/static:/static --add-data ./backend/locales:/locales --add-data ./backend/src/legacy:/src/legacy --add-data ./plugin:/plugin --hidden-import=logging.handlers --hidden-import=sqlite3 ./backend/main.py
|
||||
|
||||
working-directory: ./backend
|
||||
run: |
|
||||
poetry dynamic-versioning
|
||||
pyinstaller pyinstaller.spec
|
||||
|
||||
- name: Upload package artifact ⬆️
|
||||
if: ${{ !env.ACT }}
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: PluginLoader
|
||||
path: ./dist/PluginLoader
|
||||
path: ./backend/dist/PluginLoader
|
||||
|
||||
- name: Download package artifact locally
|
||||
if: ${{ env.ACT }}
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: ./dist/PluginLoader
|
||||
|
||||
release:
|
||||
name: Release stable version of the package
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release == 'release' }}
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout 🧰
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install semver-tool asdf
|
||||
uses: asdf-vm/actions/install@v1
|
||||
with:
|
||||
tool_versions: |
|
||||
semver 3.3.0
|
||||
|
||||
- name: Fetch package artifact ⬇️
|
||||
uses: actions/download-artifact@v3
|
||||
if: ${{ !env.ACT }}
|
||||
with:
|
||||
name: PluginLoader
|
||||
path: dist
|
||||
|
||||
- name: Get latest release
|
||||
uses: rez0n/actions-github-release@main
|
||||
id: latest_release
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: "SteamDeckHomebrew/decky-loader"
|
||||
type: "nodraft"
|
||||
|
||||
- name: Prepare tag ⚙️
|
||||
id: ready_tag
|
||||
run: |
|
||||
export VERSION=${{ steps.latest_release.outputs.release }}
|
||||
echo "VERS: $VERSION"
|
||||
OUT="notsemver"
|
||||
if [[ "$VERSION" =~ "-pre" ]]; then
|
||||
printf "is prerelease, bumping to release\n"
|
||||
OUT=$(semver bump release "$VERSION")
|
||||
printf "OUT: ${OUT}\n"\
|
||||
printf "bumping by selected type.\n"
|
||||
if [[ "${{github.event.inputs.bump}}" != "none" ]]; then
|
||||
OUT=$(semver bump ${{github.event.inputs.bump}} "$OUT")
|
||||
printf "OUT: ${OUT}\n"
|
||||
else
|
||||
printf "no type selected, not bumping for release.\n"
|
||||
fi
|
||||
elif [[ ! "$VERSION" =~ "-pre" ]]; then
|
||||
printf "previous tag is a release, bumping by selected type.\n"
|
||||
if [[ "${{github.event.inputs.bump}}" != "none" ]]; then
|
||||
OUT=$(semver bump ${{github.event.inputs.bump}} "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
else
|
||||
printf "previous tag is a release, but no bump selected. Defaulting to a patch bump.\n"
|
||||
OUT=$(semver bump patch "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
fi
|
||||
fi
|
||||
echo "vOUT: v$OUT"
|
||||
echo tag_name=v$OUT >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Push tag 📤
|
||||
uses: rickstaa/action-create-tag@v1.3.2
|
||||
if: ${{ steps.ready_tag.outputs.tag_name && github.event_name == 'workflow_dispatch' && !env.ACT }}
|
||||
with:
|
||||
tag: ${{ steps.ready_tag.outputs.tag_name }}
|
||||
message: Pre-release ${{ steps.ready_tag.outputs.tag_name }}
|
||||
|
||||
- name: Release 📦
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && !env.ACT }}
|
||||
with:
|
||||
name: Release ${{ steps.ready_tag.outputs.tag_name }}
|
||||
tag_name: ${{ steps.ready_tag.outputs.tag_name }}
|
||||
files: ./dist/PluginLoader
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
|
||||
prerelease:
|
||||
name: Release the pre-release version of the package
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release == 'prerelease' }}
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout 🧰
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install semver-tool asdf
|
||||
uses: asdf-vm/actions/install@v1
|
||||
with:
|
||||
tool_versions: |
|
||||
semver 3.3.0
|
||||
|
||||
- name: Fetch package artifact ⬇️
|
||||
uses: actions/download-artifact@v3
|
||||
if: ${{ !env.ACT }}
|
||||
with:
|
||||
name: PluginLoader
|
||||
path: dist
|
||||
|
||||
- name: Get latest release
|
||||
uses: rez0n/actions-github-release@main
|
||||
id: latest_release
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: "SteamDeckHomebrew/decky-loader"
|
||||
type: "nodraft"
|
||||
|
||||
- name: Prepare tag ⚙️
|
||||
id: ready_tag
|
||||
run: |
|
||||
export VERSION=${{ steps.latest_release.outputs.release }}
|
||||
echo "VERS: $VERSION"
|
||||
OUT=""
|
||||
if [[ ! "$VERSION" =~ "-pre" ]]; then
|
||||
printf "pre-release from release, bumping by selected type and prerel\n"
|
||||
if [[ ! ${{ github.event.inputs.bump }} == "none" ]]; then
|
||||
OUT=$(semver bump ${{github.event.inputs.bump}} "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
else
|
||||
printf "type not selected, defaulting to patch\n"
|
||||
OUT=$(semver bump patch "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
fi
|
||||
OUT="$OUT-pre"
|
||||
OUT=$(semver bump prerel "$OUT")
|
||||
printf "OUT: ${OUT}\n"
|
||||
elif [[ "$VERSION" =~ "-pre" ]]; then
|
||||
printf "pre-release to pre-release, bumping by selected type and or prerel version\n"
|
||||
if [[ ! ${{ github.event.inputs.bump }} == "none" ]]; then
|
||||
OUT=$(semver bump ${{github.event.inputs.bump}} "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
OUT="$OUT-pre"
|
||||
printf "OUT: ${OUT}\n"
|
||||
printf "bumping prerel\n"
|
||||
OUT=$(semver bump prerel "$OUT")
|
||||
printf "OUT: ${OUT}\n"
|
||||
else
|
||||
printf "type not selected, defaulting to new pre-release only\n"
|
||||
printf "bumping prerel\n"
|
||||
OUT=$(semver bump prerel "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
fi
|
||||
fi
|
||||
printf "vOUT: v${OUT}\n"
|
||||
echo tag_name=v$OUT >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Push tag 📤
|
||||
uses: rickstaa/action-create-tag@v1.3.2
|
||||
if: ${{ steps.ready_tag.outputs.tag_name && github.event_name == 'workflow_dispatch' && !env.ACT }}
|
||||
with:
|
||||
tag: ${{ steps.ready_tag.outputs.tag_name }}
|
||||
message: Pre-release ${{ steps.ready_tag.outputs.tag_name }}
|
||||
|
||||
- name: Release 📦
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && !env.ACT }}
|
||||
with:
|
||||
name: Prerelease ${{ steps.ready_tag.outputs.tag_name }}
|
||||
tag_name: ${{ steps.ready_tag.outputs.tag_name }}
|
||||
files: ./dist/PluginLoader
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
path: ./backend/dist/PluginLoader
|
||||
|
||||
@@ -4,7 +4,7 @@ on: push
|
||||
|
||||
jobs:
|
||||
copy-stub:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8230315d06ad95c617244d2f265d237a1682d445
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
@@ -22,18 +22,18 @@ jobs:
|
||||
with:
|
||||
separator: ","
|
||||
files: |
|
||||
plugin/*
|
||||
backend/decky_loader/plugin/imports/decky.pyi
|
||||
|
||||
- name: Is stub changed
|
||||
id: changed-stub
|
||||
run: |
|
||||
STUB_CHANGED="false"
|
||||
PATHS=(plugin plugin/decky_plugin.pyi)
|
||||
PATHS=(backend backend/decky_loader/plugin/imports/decky.pyi)
|
||||
SHA=${{ github.sha }}
|
||||
SHA_PREV=HEAD^
|
||||
FILES=$(git diff $SHA_PREV..$SHA --name-only -- ${PATHS[@]} | jq -Rsc 'split("\n")[:-1] | join (",")')
|
||||
if [[ "$FILES" == *"plugin/decky_plugin.pyi"* ]]; then
|
||||
$STUB_CHANGED="true"
|
||||
if [[ "$FILES" == *"backend/decky_loader/plugin/imports/decky.pyi"* ]]; then
|
||||
STUB_CHANGED="true"
|
||||
echo "Stub has changed, pushing updated stub"
|
||||
else
|
||||
echo "Stub has not changed, exiting."
|
||||
@@ -43,12 +43,12 @@ jobs:
|
||||
echo "has_changed=$STUB_CHANGED" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Push updated stub
|
||||
if: steps.changed-stub.outputs.has_changed == true
|
||||
if: github.ref == 'refs/heads/main' && steps.changed-stub.outputs.has_changed == true
|
||||
uses: dmnemec/copy_file_to_another_repo_action@bbebd3da22e4a37d04dca5f782edd5201cb97083
|
||||
env:
|
||||
API_TOKEN_GITHUB: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
source_file: 'plugin/decky_plugin.pyi'
|
||||
source_file: 'backend/decky_loader/plugin/imports/decky.pyi'
|
||||
destination_repo: 'SteamDeckHomebrew/decky-plugin-template'
|
||||
user_email: '11465594+TrainDoctor@users.noreply.github.com'
|
||||
user_name: 'TrainDoctor'
|
||||
|
||||
@@ -7,10 +7,10 @@ on:
|
||||
jobs:
|
||||
lint:
|
||||
name: Run linters
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3 # Check out the repository first.
|
||||
- uses: actions/checkout@v4 # Check out the repository first.
|
||||
|
||||
- name: Install TypeScript dependencies
|
||||
working-directory: frontend
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release:
|
||||
type: choice
|
||||
description: Release the asset
|
||||
default: 'none'
|
||||
options:
|
||||
- none
|
||||
- prerelease
|
||||
- release
|
||||
bump:
|
||||
type: choice
|
||||
description: Semver to bump
|
||||
default: 'none'
|
||||
options:
|
||||
- none
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
create_tag:
|
||||
name: Tag a new version of the package
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
tag_name: ${{ steps.ready_tag.outputs.tag_name }}
|
||||
|
||||
steps:
|
||||
- name: Checkout 🧰
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install semver-tool asdf
|
||||
uses: asdf-vm/actions/install@v1
|
||||
with:
|
||||
tool_versions: |
|
||||
semver 3.3.0
|
||||
|
||||
- name: Get latest release
|
||||
uses: rez0n/actions-github-release@main
|
||||
id: latest_release
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: "SteamDeckHomebrew/decky-loader"
|
||||
type: "nodraft"
|
||||
|
||||
- name: Prepare tag ⚙️
|
||||
id: ready_tag
|
||||
run: |
|
||||
export VERSION=${{ steps.latest_release.outputs.release }}
|
||||
echo "VERS: $VERSION"
|
||||
if [[ ${{github.event.inputs.release}} == "release" ]]; then
|
||||
OUT="notsemver"
|
||||
if [[ "$VERSION" =~ "-pre" ]]; then
|
||||
printf "is prerelease, bumping to release\n"
|
||||
OUT=$(semver bump release "$VERSION")
|
||||
printf "OUT: ${OUT}\n"\
|
||||
printf "bumping by selected type.\n"
|
||||
if [[ "${{github.event.inputs.bump}}" != "none" ]]; then
|
||||
OUT=$(semver bump ${{github.event.inputs.bump}} "$OUT")
|
||||
printf "OUT: ${OUT}\n"
|
||||
else
|
||||
printf "no type selected, not bumping for release.\n"
|
||||
fi
|
||||
elif [[ ! "$VERSION" =~ "-pre" ]]; then
|
||||
printf "previous tag is a release, bumping by selected type.\n"
|
||||
if [[ "${{github.event.inputs.bump}}" != "none" ]]; then
|
||||
OUT=$(semver bump ${{github.event.inputs.bump}} "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
else
|
||||
printf "previous tag is a release, but no bump selected. Defaulting to a patch bump.\n"
|
||||
OUT=$(semver bump patch "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
OUT=""
|
||||
if [[ ! "$VERSION" =~ "-pre" ]]; then
|
||||
printf "pre-release from release, bumping by selected type and prerel\n"
|
||||
if [[ ! ${{ github.event.inputs.bump }} == "none" ]]; then
|
||||
OUT=$(semver bump ${{github.event.inputs.bump}} "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
else
|
||||
printf "type not selected, defaulting to patch\n"
|
||||
OUT=$(semver bump patch "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
fi
|
||||
OUT="$OUT-pre"
|
||||
OUT=$(semver bump prerel "$OUT")
|
||||
printf "OUT: ${OUT}\n"
|
||||
elif [[ "$VERSION" =~ "-pre" ]]; then
|
||||
printf "pre-release to pre-release, bumping by selected type and or prerel version\n"
|
||||
if [[ ! ${{ github.event.inputs.bump }} == "none" ]]; then
|
||||
OUT=$(semver bump ${{github.event.inputs.bump}} "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
OUT="$OUT-pre"
|
||||
printf "OUT: ${OUT}\n"
|
||||
printf "bumping prerel\n"
|
||||
OUT=$(semver bump prerel "$OUT")
|
||||
printf "OUT: ${OUT}\n"
|
||||
else
|
||||
printf "type not selected, defaulting to new pre-release only\n"
|
||||
printf "bumping prerel\n"
|
||||
OUT=$(semver bump prerel "$VERSION")
|
||||
printf "OUT: ${OUT}\n"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
echo "vOUT: v${OUT}"
|
||||
echo tag_name=v$OUT >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Push tag 📤
|
||||
uses: rickstaa/action-create-tag@v1.3.2
|
||||
if: ${{ steps.ready_tag.outputs.tag_name && !env.ACT }}
|
||||
with:
|
||||
tag: ${{ steps.ready_tag.outputs.tag_name }}
|
||||
message: Pre-release ${{ steps.ready_tag.outputs.tag_name }}
|
||||
|
||||
build:
|
||||
name: Build tagged artifact
|
||||
uses: ./.github/workflows/build.yml
|
||||
needs: [create_tag]
|
||||
|
||||
release:
|
||||
name: Release tagged artifact
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [create_tag, build]
|
||||
steps:
|
||||
- name: Fetch package artifact ⬇️
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: PluginLoader
|
||||
|
||||
- name: Pre-release 📦
|
||||
if: github.event.inputs.release == 'prerelease'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: Prerelease ${{ needs.create_tag.outputs.tag_name }}
|
||||
tag_name: ${{ needs.create_tag.outputs.tag_name }}
|
||||
files: ./PluginLoader
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
- name: Release 📦
|
||||
if: github.event.inputs.release == 'release'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: Release ${{ needs.create_tag.outputs.tag_name }}
|
||||
tag_name: ${{ needs.create_tag.outputs.tag_name }}
|
||||
files: ./PluginLoader
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
@@ -7,16 +7,29 @@ on:
|
||||
jobs:
|
||||
typecheck:
|
||||
name: Run type checkers
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2 # Check out the repository first.
|
||||
- uses: actions/checkout@v4 # Check out the repository first.
|
||||
|
||||
- name: Install Python dependencies
|
||||
- name: Set up NodeJS 20 💎
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Set up Python 3.11.7 🐍
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11.7"
|
||||
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
virtualenvs-create: false
|
||||
|
||||
- name: Install Python dependencies ⬇️
|
||||
working-directory: backend
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
[ -f requirements.txt ] && pip install -r requirements.txt
|
||||
run: poetry install --no-interaction
|
||||
|
||||
- name: Install TypeScript dependencies
|
||||
working-directory: frontend
|
||||
@@ -33,4 +46,4 @@ jobs:
|
||||
|
||||
- name: Run tsc (TypeScript)
|
||||
working-directory: frontend
|
||||
run: $(pnpm bin)/tsc --noEmit
|
||||
run: pnpm run typecheck
|
||||
+7
-4
@@ -29,7 +29,7 @@ MANIFEST
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
backend/dist/
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
@@ -126,6 +126,7 @@ venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
.direnv/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
@@ -152,13 +153,15 @@ dmypy.json
|
||||
cython_debug/
|
||||
|
||||
# static files are built
|
||||
backend/static
|
||||
backend/decky_loader/static
|
||||
|
||||
# ignore settings.json
|
||||
# prevents leaking login details
|
||||
.vscode/settings.json
|
||||
|
||||
# plugins folder for local launches
|
||||
plugins/*
|
||||
/plugins/*
|
||||
act/.directory
|
||||
act/artifacts/*
|
||||
act/artifacts/*
|
||||
bin/act
|
||||
/settings/
|
||||
|
||||
Vendored
+1
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"deckip" : "0.0.0.0",
|
||||
"deckport" : "22",
|
||||
"deckuser" : "deck",
|
||||
"deckpass" : "ssap",
|
||||
"deckkey" : "-i ${env:HOME}/.ssh/id_rsa",
|
||||
"deckdir" : "/home/deck"
|
||||
|
||||
Vendored
+17
-5
@@ -37,8 +37,11 @@
|
||||
"label": "dependencies",
|
||||
"type": "shell",
|
||||
"group": "none",
|
||||
"dependsOn": [
|
||||
"deploy"
|
||||
],
|
||||
"detail": "Check for local runs, create a plugins folder",
|
||||
"command": "rsync -azp --rsh='ssh -p ${config:deckport} ${config:deckkey}' backend/requirements.txt deck@${config:deckip}:${config:deckdir}/homebrew/dev/pluginloader/backend/requirements.txt && ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'python -m ensurepip && python -m pip install --upgrade --break-system-packages pip && python -m pip install --break-system-packages --upgrade setuptools && python -m pip install --break-system-packages -r ${config:deckdir}/homebrew/dev/pluginloader/backend/requirements.txt'",
|
||||
"command": "ssh ${config:deckuser}@${config:deckip} -p ${config:deckport} ${config:deckkey} 'python -m ensurepip --root / && python -m pip install --user --break-system-packages --upgrade poetry && cd ${config:deckdir}/homebrew/dev/pluginloader/backend && python -m poetry install'",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
@@ -97,7 +100,7 @@
|
||||
"dependsOn": [
|
||||
"checkforsettings"
|
||||
],
|
||||
"command": "ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'mkdir -p ${config:deckdir}/homebrew/dev/pluginloader && mkdir -p ${config:deckdir}/homebrew/dev/plugins'",
|
||||
"command": "ssh ${config:deckuser}@${config:deckip} -p ${config:deckport} ${config:deckkey} 'mkdir -p ${config:deckdir}/homebrew/dev/pluginloader && mkdir -p ${config:deckdir}/homebrew/plugins'",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
@@ -105,7 +108,7 @@
|
||||
"detail": "Deploy dev PluginLoader to deck",
|
||||
"type": "shell",
|
||||
"group": "none",
|
||||
"command": "rsync -azp --delete --rsh='ssh -p ${config:deckport} ${config:deckkey}' --exclude='.git/' --exclude='.github/' --exclude='.vscode/' --exclude='frontend/' --exclude='dist/' --exclude='contrib/' --exclude='*.log' --exclude='requirements.txt' --exclude='**/__pycache__/' --exclude='.gitignore' . deck@${config:deckip}:${config:deckdir}/homebrew/dev/pluginloader",
|
||||
"command": "rsync -azp --delete --force --rsh='ssh -p ${config:deckport} ${config:deckkey}' --exclude='.git/' --exclude='.github/' --exclude='.vscode/' --exclude='frontend/' --exclude='dist/' --exclude='contrib/' --exclude='*.log' --exclude='**/__pycache__/' --exclude='.gitignore' . ${config:deckuser}@${config:deckip}:${config:deckdir}/homebrew/dev/pluginloader",
|
||||
"problemMatcher": []
|
||||
},
|
||||
// RUN
|
||||
@@ -117,7 +120,7 @@
|
||||
"dependsOn": [
|
||||
"checkforsettings"
|
||||
],
|
||||
"command": "ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'export PLUGIN_PATH=${config:deckdir}/homebrew/dev/plugins; export CHOWN_PLUGIN_PATH=0; export LOG_LEVEL=DEBUG; cd ${config:deckdir}/homebrew/services; echo '${config:deckpass}' | sudo -SE python3 ${config:deckdir}/homebrew/dev/pluginloader/backend/main.py'",
|
||||
"command": "ssh ${config:deckuser}@${config:deckip} -p ${config:deckport} ${config:deckkey} 'export PATH=${config:deckdir}/.local/bin:$PATH; export PLUGIN_PATH=${config:deckdir}/homebrew/plugins; export CHOWN_PLUGIN_PATH=0; export LOG_LEVEL=DEBUG; cd ${config:deckdir}/homebrew/dev/pluginloader/backend; echo '${config:deckpass}' | poetry run sh -c \"cd ${config:deckdir}/homebrew/services; sudo -SE env \"PATH=\\$PATH\" python3 ${config:deckdir}/homebrew/dev/pluginloader/backend/main.py\"'",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
@@ -181,10 +184,19 @@
|
||||
"buildall",
|
||||
"createfolders",
|
||||
"dependencies",
|
||||
"deploy",
|
||||
// dependencies runs deploy already
|
||||
// "deploy",
|
||||
"runpydeck"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "act",
|
||||
"type": "shell",
|
||||
"group": "none",
|
||||
"detail": "Build release artifact using local CI",
|
||||
"command": "./act/run-act.sh release",
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
+21
-18
@@ -1,26 +1,29 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
type=$1
|
||||
# bump=$2
|
||||
|
||||
oldartifactsdir="old"
|
||||
|
||||
parent_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )
|
||||
cd "$parent_path"
|
||||
parent_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" || exit ; pwd -P )
|
||||
cd "$parent_path" || exit
|
||||
|
||||
artifactfolders=$(find artifacts/ -maxdepth 1 -mindepth 1 -type d)
|
||||
if [[ ${#artifactfolders[@]} > 0 ]]; then
|
||||
for i in ${artifactfolders[@]}; do
|
||||
foldername=$(dirname $i)
|
||||
subfoldername=$(basename $i)
|
||||
out=$foldername/$oldartifactsdir/$subfoldername-$(date +'%s')
|
||||
if [[ ! "$subfoldername" =~ "$oldartifactsdir" ]]; then
|
||||
mkdir -p $out
|
||||
mv $i $out
|
||||
printf "Moved "${foldername}"/"${subfoldername}" to "${out}" \n"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
for i in artifacts/*; do
|
||||
if [[ ! -d "$i" ]]; then
|
||||
continue;
|
||||
fi
|
||||
subfoldername=$(basename "$i")
|
||||
|
||||
if [[ "$subfoldername" == "$oldartifactsdir" ]]; then
|
||||
continue;
|
||||
fi
|
||||
|
||||
out=artifacts/$oldartifactsdir/$subfoldername-$(date +'%s')
|
||||
mkdir -p "$out"
|
||||
mv "$i" "$out"
|
||||
echo "Moved artifacts/${subfoldername} to ${out}"
|
||||
done
|
||||
|
||||
cd ..
|
||||
|
||||
@@ -35,10 +38,10 @@ else
|
||||
printf "Options: 'release' or 'prerelease'\n"
|
||||
fi
|
||||
|
||||
cd act/artifacts
|
||||
cd act/artifacts || exit
|
||||
|
||||
if [[ -d "1" ]]; then
|
||||
cd "1/artifact"
|
||||
cd "1/artifact" || exit
|
||||
cp "PluginLoader.gz__" "PluginLoader.gz"
|
||||
gzip -d "PluginLoader.gz"
|
||||
chmod +x PluginLoader
|
||||
|
||||
@@ -18,11 +18,10 @@ from enum import IntEnum
|
||||
from typing import Dict, List, TypedDict
|
||||
|
||||
# Local modules
|
||||
from .localplatform import chown, chmod
|
||||
from .localplatform.localplatform import chown, chmod
|
||||
from .loader import Loader, Plugins
|
||||
from .helpers import get_ssl_context, download_remote_binary_to_path
|
||||
from .settings import SettingsManager
|
||||
from .injector import get_gamepadui_tab
|
||||
|
||||
logger = getLogger("Browser")
|
||||
|
||||
@@ -123,7 +122,6 @@ class PluginBrowser:
|
||||
async def uninstall_plugin(self, name: str):
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = True
|
||||
tab = await get_gamepadui_tab()
|
||||
plugin_folder = self.find_plugin_folder(name)
|
||||
assert plugin_folder is not None
|
||||
plugin_dir = path.join(self.plugin_path, plugin_folder)
|
||||
@@ -131,14 +129,13 @@ class PluginBrowser:
|
||||
logger.info("uninstalling " + name)
|
||||
logger.info(" at dir " + plugin_dir)
|
||||
logger.debug("calling frontend unload for %s" % str(name))
|
||||
res = await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
|
||||
logger.debug("result of unload from UI: %s", res)
|
||||
await self.loader.ws.emit("loader/unload_plugin", name)
|
||||
# plugins_snapshot = self.plugins.copy()
|
||||
# snapshot_string = pformat(plugins_snapshot)
|
||||
# logger.debug("current plugins: %s", snapshot_string)
|
||||
if name in self.plugins:
|
||||
logger.debug("Plugin %s was found", name)
|
||||
self.plugins[name].stop(uninstall=True)
|
||||
await self.plugins[name].stop(uninstall=True)
|
||||
logger.debug("Plugin %s was stopped", name)
|
||||
del self.plugins[name]
|
||||
logger.debug("Plugin %s was removed from the dictionary", name)
|
||||
@@ -154,6 +151,8 @@ class PluginBrowser:
|
||||
self.loader.watcher.disabled = False
|
||||
|
||||
async def _install(self, artifact: str, name: str, version: str, hash: str):
|
||||
await self.loader.ws.emit("loader/plugin_download_start", name)
|
||||
await self.loader.ws.emit("loader/plugin_download_info", 5, "Store.download_progress_info.start")
|
||||
# Will be set later in code
|
||||
res_zip = None
|
||||
|
||||
@@ -167,12 +166,17 @@ class PluginBrowser:
|
||||
# Check if the file is a local file or a URL
|
||||
if artifact.startswith("file://"):
|
||||
logger.info(f"Installing {name} from local ZIP file (Version: {version})")
|
||||
await self.loader.ws.emit("loader/plugin_download_info", 10, "Store.download_progress_info.open_zip")
|
||||
res_zip = BytesIO(open(artifact[7:], "rb").read())
|
||||
else:
|
||||
logger.info(f"Installing {name} from URL (Version: {version})")
|
||||
await self.loader.ws.emit("loader/plugin_download_info", 10, "Store.download_progress_info.download_zip")
|
||||
|
||||
async with ClientSession() as client:
|
||||
logger.debug(f"Fetching {artifact}")
|
||||
res = await client.get(artifact, ssl=get_ssl_context())
|
||||
#TODO track progress of this download in chunks like with decky updates
|
||||
#TODO but squish with min 15 and max 75
|
||||
if res.status == 200:
|
||||
logger.debug("Got 200. Reading...")
|
||||
data = await res.read()
|
||||
@@ -181,6 +185,7 @@ class PluginBrowser:
|
||||
else:
|
||||
logger.fatal(f"Could not fetch from URL. {await res.text()}")
|
||||
|
||||
await self.loader.ws.emit("loader/plugin_download_info", 80, "Store.download_progress_info.increment_count")
|
||||
storeUrl = ""
|
||||
match self.settings.getSetting("store", 0):
|
||||
case 0: storeUrl = "https://plugins.deckbrew.xyz/plugins" # default
|
||||
@@ -193,6 +198,7 @@ class PluginBrowser:
|
||||
if res.status != 200:
|
||||
logger.error(f"Server did not accept install count increment request. code: {res.status}")
|
||||
|
||||
await self.loader.ws.emit("loader/plugin_download_info", 85, "Store.download_progress_info.parse_zip")
|
||||
if res_zip and version == "dev":
|
||||
with ZipFile(res_zip) as plugin_zip:
|
||||
plugin_json_list = [file for file in plugin_zip.namelist() if file.endswith("/plugin.json") and file.count("/") == 1]
|
||||
@@ -222,12 +228,15 @@ class PluginBrowser:
|
||||
|
||||
# If plugin is installed, uninstall it
|
||||
if isInstalled:
|
||||
await self.loader.ws.emit("loader/plugin_download_info", 90, "Store.download_progress_info.uninstalling_previous")
|
||||
try:
|
||||
logger.debug("Uninstalling existing plugin...")
|
||||
await self.uninstall_plugin(name)
|
||||
except:
|
||||
logger.error(f"Plugin {name} could not be uninstalled.")
|
||||
|
||||
|
||||
await self.loader.ws.emit("loader/plugin_download_info", 95, "Store.download_progress_info.installing_plugin")
|
||||
# Install the plugin
|
||||
logger.debug("Unzipping...")
|
||||
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
|
||||
@@ -235,43 +244,39 @@ class PluginBrowser:
|
||||
plugin_folder = self.find_plugin_folder(name)
|
||||
assert plugin_folder is not None
|
||||
plugin_dir = path.join(self.plugin_path, plugin_folder)
|
||||
#TODO count again from 0% to 100% quickly for this one if it does anything
|
||||
ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir)
|
||||
if ret:
|
||||
logger.info(f"Installed {name} (Version: {version})")
|
||||
if name in self.loader.plugins:
|
||||
self.loader.plugins[name].stop()
|
||||
await self.loader.plugins[name].stop()
|
||||
self.loader.plugins.pop(name, None)
|
||||
await sleep(1)
|
||||
if not isInstalled:
|
||||
current_plugin_order = self.settings.getSetting("pluginOrder")
|
||||
current_plugin_order.append(name)
|
||||
self.settings.setSetting("pluginOrder", current_plugin_order)
|
||||
logger.debug("Plugin %s was added to the pluginOrder setting", name)
|
||||
self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
|
||||
self.settings.setSetting("pluginOrder", current_plugin_order)
|
||||
logger.debug("Plugin %s was added to the pluginOrder setting", name)
|
||||
await self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
|
||||
else:
|
||||
logger.fatal(f"Failed Downloading Remote Binaries")
|
||||
else:
|
||||
logger.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = False
|
||||
await self.loader.ws.emit("loader/plugin_download_finish", name)
|
||||
|
||||
async def request_plugin_install(self, artifact: str, name: str, version: str, hash: str, install_type: PluginInstallType):
|
||||
request_id = str(time())
|
||||
self.install_requests[request_id] = PluginInstallContext(artifact, name, version, hash)
|
||||
tab = await get_gamepadui_tab()
|
||||
await tab.open_websocket()
|
||||
await tab.evaluate_js(f"DeckyPluginLoader.addPluginInstallPrompt('{name}', '{version}', '{request_id}', '{hash}', {install_type})")
|
||||
|
||||
await self.loader.ws.emit("loader/add_plugin_install_prompt", name, version, request_id, hash, install_type)
|
||||
|
||||
async def request_multiple_plugin_installs(self, requests: List[PluginInstallRequest]):
|
||||
request_id = str(time())
|
||||
self.install_requests[request_id] = [PluginInstallContext(req['artifact'], req['name'], req['version'], req['hash']) for req in requests]
|
||||
js_requests_parameter = ','.join([
|
||||
f"{{ name: '{req['name']}', version: '{req['version']}', hash: '{req['hash']}', install_type: {req['install_type']}}}" for req in requests
|
||||
])
|
||||
|
||||
tab = await get_gamepadui_tab()
|
||||
await tab.open_websocket()
|
||||
await tab.evaluate_js(f"DeckyPluginLoader.addMultiplePluginsInstallPrompt('{request_id}', [{js_requests_parameter}])")
|
||||
await self.loader.ws.emit("loader/add_multiple_plugins_install_prompt", request_id, requests)
|
||||
|
||||
async def confirm_plugin_install(self, request_id: str):
|
||||
requestOrRequests = self.install_requests.pop(request_id)
|
||||
@@ -0,0 +1,10 @@
|
||||
from enum import IntEnum
|
||||
|
||||
class UserType(IntEnum):
|
||||
HOST_USER = 1
|
||||
EFFECTIVE_USER = 2
|
||||
ROOT = 3
|
||||
|
||||
class PluginLoadType(IntEnum):
|
||||
LEGACY_EVAL_IIFE = 0 # legacy, uses legacy serverAPI
|
||||
ESMODULE_V1 = 1 # esmodule loading with modern @decky/backend apis
|
||||
@@ -5,15 +5,18 @@ import os
|
||||
import subprocess
|
||||
from hashlib import sha256
|
||||
from io import BytesIO
|
||||
import importlib.metadata
|
||||
|
||||
import certifi
|
||||
from aiohttp.web import Request, Response, middleware
|
||||
from aiohttp.typedefs import Handler
|
||||
from aiohttp import ClientSession
|
||||
from . import localplatform
|
||||
from .customtypes import UserType
|
||||
from .localplatform import localplatform
|
||||
from .enums import UserType
|
||||
from logging import getLogger
|
||||
from packaging.version import Version
|
||||
|
||||
SSHD_UNIT = "sshd.service"
|
||||
REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service"
|
||||
|
||||
# global vars
|
||||
@@ -21,6 +24,7 @@ csrf_token = str(uuid.uuid4())
|
||||
ssl_ctx = ssl.create_default_context(cafile=certifi.where())
|
||||
|
||||
assets_regex = re.compile("^/plugins/.*/assets/.*")
|
||||
dist_regex = re.compile("^/plugins/.*/dist/.*")
|
||||
frontend_regex = re.compile("^/frontend/.*")
|
||||
logger = getLogger("Main")
|
||||
|
||||
@@ -32,7 +36,19 @@ def get_csrf_token():
|
||||
|
||||
@middleware
|
||||
async def csrf_middleware(request: Request, handler: Handler):
|
||||
if str(request.method) == "OPTIONS" or request.headers.get('Authentication') == csrf_token or str(request.rel_url) == "/auth/token" or str(request.rel_url).startswith("/plugins/load_main/") or str(request.rel_url).startswith("/static/") or str(request.rel_url).startswith("/legacy/") or str(request.rel_url).startswith("/steam_resource/") or str(request.rel_url).startswith("/frontend/") or assets_regex.match(str(request.rel_url)) or frontend_regex.match(str(request.rel_url)):
|
||||
if str(request.method) == "OPTIONS" or \
|
||||
request.headers.get('X-Decky-Auth') == csrf_token or \
|
||||
str(request.rel_url) == "/auth/token" or \
|
||||
str(request.rel_url).startswith("/plugins/load_main/") or \
|
||||
str(request.rel_url).startswith("/static/") or \
|
||||
str(request.rel_url).startswith("/steam_resource/") or \
|
||||
str(request.rel_url).startswith("/frontend/") or \
|
||||
str(request.rel_url.path) == "/fetch" or \
|
||||
str(request.rel_url.path) == "/ws" or \
|
||||
assets_regex.match(str(request.rel_url)) or \
|
||||
dist_regex.match(str(request.rel_url)) or \
|
||||
frontend_regex.match(str(request.rel_url)):
|
||||
|
||||
return await handler(request)
|
||||
return Response(text='Forbidden', status=403)
|
||||
|
||||
@@ -49,19 +65,31 @@ def mkdir_as_user(path: str):
|
||||
# Fetches the version of loader
|
||||
def get_loader_version() -> str:
|
||||
try:
|
||||
with open(os.path.join(os.getcwd(), ".loader.version"), "r", encoding="utf-8") as version_file:
|
||||
return version_file.readline().strip()
|
||||
# Normalize Python-style version to conform to Decky style
|
||||
v = Version(importlib.metadata.version("decky_loader"))
|
||||
|
||||
version_str = f'v{v.major}.{v.minor}.{v.micro}'
|
||||
|
||||
if v.pre:
|
||||
version_str += f'-pre{v.pre[1]}'
|
||||
|
||||
if v.post:
|
||||
version_str += f'-dev{v.post}'
|
||||
|
||||
return version_str
|
||||
except Exception as e:
|
||||
logger.warn(f"Failed to execute get_loader_version(): {str(e)}")
|
||||
return "unknown"
|
||||
|
||||
user_agent = f"Decky/{get_loader_version()} (https://decky.xyz)"
|
||||
|
||||
# returns the appropriate system python paths
|
||||
def get_system_pythonpaths() -> list[str]:
|
||||
try:
|
||||
# run as normal normal user if on linux to also include user python paths
|
||||
proc = subprocess.run(["python3" if localplatform.ON_LINUX else "python", "-c", "import sys; print('\\n'.join(x for x in sys.path if x))"],
|
||||
# TODO make this less insane
|
||||
capture_output=True, user=localplatform.localplatform._get_user_id() if localplatform.ON_LINUX else None, env={} if localplatform.ON_LINUX else None) # type: ignore
|
||||
capture_output=True, user=localplatform.localplatform._get_user_id() if localplatform.ON_LINUX else None, env={} if localplatform.ON_LINUX else None) # pyright: ignore [reportPrivateUsage]
|
||||
return [x.strip() for x in proc.stdout.decode().strip().split("\n")]
|
||||
except Exception as e:
|
||||
logger.warn(f"Failed to execute get_system_pythonpaths(): {str(e)}")
|
||||
@@ -33,7 +33,7 @@ class Tab:
|
||||
|
||||
async def open_websocket(self):
|
||||
self.client = ClientSession()
|
||||
self.websocket = await self.client.ws_connect(self.ws_url) # type: ignore
|
||||
self.websocket = await self.client.ws_connect(self.ws_url)
|
||||
|
||||
async def close_websocket(self):
|
||||
if self.websocket:
|
||||
@@ -412,7 +412,7 @@ async def get_tab_lambda(test: Callable[[Tab], bool]) -> Tab:
|
||||
|
||||
SHARED_CTX_NAMES = ["SharedJSContext", "Steam Shared Context presented by Valve™", "Steam", "SP"]
|
||||
CLOSEABLE_URLS = ["about:blank", "data:text/html,%3Cbody%3E%3C%2Fbody%3E"] # Closing anything other than these *really* likes to crash Steam
|
||||
DO_NOT_CLOSE_URLS = ["Valve Steam Gamepad/default", "Valve%20Steam%20Gamepad/default"] # Steam Big Picture Mode tab
|
||||
DO_NOT_CLOSE_URLS = ["Valve Steam Gamepad/default", "Valve%20Steam%20Gamepad"] # Steam Big Picture Mode tab
|
||||
|
||||
def tab_is_gamepadui(t: Tab) -> bool:
|
||||
return "https://steamloopback.host/routes/" in t.url and t.title in SHARED_CTX_NAMES
|
||||
@@ -1,30 +1,30 @@
|
||||
from __future__ import annotations
|
||||
from asyncio import AbstractEventLoop, Queue, sleep
|
||||
from json.decoder import JSONDecodeError
|
||||
from logging import getLogger
|
||||
from os import listdir, path
|
||||
from pathlib import Path
|
||||
from traceback import print_exc
|
||||
from typing import Any, Tuple
|
||||
from traceback import print_exc, format_exc
|
||||
from typing import Any, Tuple, Dict, cast
|
||||
|
||||
from aiohttp import web
|
||||
from os.path import exists
|
||||
from watchdog.events import RegexMatchingEventHandler, DirCreatedEvent, DirModifiedEvent, FileCreatedEvent, FileModifiedEvent # type: ignore
|
||||
from watchdog.observers import Observer # type: ignore
|
||||
from watchdog.events import RegexMatchingEventHandler, FileSystemEvent
|
||||
from watchdog.observers import Observer
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, List
|
||||
if TYPE_CHECKING:
|
||||
from .main import PluginManager
|
||||
|
||||
from .injector import get_tab, get_gamepadui_tab
|
||||
from .plugin import PluginWrapper
|
||||
from .plugin.plugin import PluginWrapper
|
||||
from .wsrouter import WSRouter
|
||||
from .enums import PluginLoadType
|
||||
|
||||
Plugins = dict[str, PluginWrapper]
|
||||
ReloadQueue = Queue[Tuple[str, str, bool | None] | Tuple[str, str]]
|
||||
|
||||
class FileChangeHandler(RegexMatchingEventHandler):
|
||||
def __init__(self, queue: ReloadQueue, plugin_path: str) -> None:
|
||||
super().__init__(regexes=[r'^.*?dist\/index\.js$', r'^.*?main\.py$']) # type: ignore
|
||||
super().__init__(regexes=[r'^.*?dist\/index\.js$', r'^.*?main\.py$']) # pyright: ignore [reportUnknownMemberType]
|
||||
self.logger = getLogger("file-watcher")
|
||||
self.plugin_path = plugin_path
|
||||
self.queue = queue
|
||||
@@ -37,8 +37,8 @@ class FileChangeHandler(RegexMatchingEventHandler):
|
||||
if exists(path.join(self.plugin_path, plugin_dir, "plugin.json")):
|
||||
self.queue.put_nowait((path.join(self.plugin_path, plugin_dir, "main.py"), plugin_dir, True))
|
||||
|
||||
def on_created(self, event: DirCreatedEvent | FileCreatedEvent):
|
||||
src_path = event.src_path
|
||||
def on_created(self, event: FileSystemEvent):
|
||||
src_path = cast(str, event.src_path) #type: ignore # this is the correct type for this is in later versions of watchdog
|
||||
if "__pycache__" in src_path:
|
||||
return
|
||||
|
||||
@@ -51,8 +51,8 @@ class FileChangeHandler(RegexMatchingEventHandler):
|
||||
self.logger.debug(f"file created: {src_path}")
|
||||
self.maybe_reload(src_path)
|
||||
|
||||
def on_modified(self, event: DirModifiedEvent | FileModifiedEvent):
|
||||
src_path = event.src_path
|
||||
def on_modified(self, event: FileSystemEvent):
|
||||
src_path = cast(str, event.src_path) # type: ignore
|
||||
if "__pycache__" in src_path:
|
||||
return
|
||||
|
||||
@@ -66,9 +66,10 @@ class FileChangeHandler(RegexMatchingEventHandler):
|
||||
self.maybe_reload(src_path)
|
||||
|
||||
class Loader:
|
||||
def __init__(self, server_instance: PluginManager, plugin_path: str, loop: AbstractEventLoop, live_reload: bool = False) -> None:
|
||||
def __init__(self, server_instance: PluginManager, ws: WSRouter, plugin_path: str, loop: AbstractEventLoop, live_reload: bool = False) -> None:
|
||||
self.loop = loop
|
||||
self.logger = getLogger("Loader")
|
||||
self.ws = ws
|
||||
self.plugin_path = plugin_path
|
||||
self.logger.info(f"plugin_path: {self.plugin_path}")
|
||||
self.plugins: Plugins = {}
|
||||
@@ -80,25 +81,23 @@ class Loader:
|
||||
if live_reload:
|
||||
self.observer = Observer()
|
||||
self.watcher = FileChangeHandler(self.reload_queue, plugin_path)
|
||||
self.observer.schedule(self.watcher, self.plugin_path, recursive=True) # type: ignore
|
||||
self.observer.schedule(self.watcher, self.plugin_path, recursive=True) # pyright: ignore [reportUnknownMemberType]
|
||||
self.observer.start()
|
||||
self.loop.create_task(self.enable_reload_wait())
|
||||
|
||||
|
||||
server_instance.web_app.add_routes([
|
||||
web.get("/frontend/{path:.*}", self.handle_frontend_assets),
|
||||
web.get("/locales/{path:.*}", self.handle_frontend_locales),
|
||||
web.get("/plugins", self.get_plugins),
|
||||
web.get("/plugins/{plugin_name}/frontend_bundle", self.handle_frontend_bundle),
|
||||
web.post("/plugins/{plugin_name}/methods/{method_name}", self.handle_plugin_method_call),
|
||||
web.get("/plugins/{plugin_name}/dist/{path:.*}", self.handle_plugin_dist),
|
||||
web.get("/plugins/{plugin_name}/assets/{path:.*}", self.handle_plugin_frontend_assets),
|
||||
web.post("/plugins/{plugin_name}/reload", self.handle_backend_reload_request),
|
||||
|
||||
# The following is legacy plugin code.
|
||||
web.get("/plugins/load_main/{name}", self.load_plugin_main_view),
|
||||
web.get("/plugins/plugin_resource/{name}/{path:.+}", self.handle_sub_route),
|
||||
web.get("/steam_resource/{path:.+}", self.get_steam_resource)
|
||||
])
|
||||
|
||||
server_instance.ws.add_route("loader/get_plugins", self.get_plugins)
|
||||
server_instance.ws.add_route("loader/reload_plugin", self.handle_plugin_backend_reload)
|
||||
server_instance.ws.add_route("loader/call_plugin_method", self.handle_plugin_method_call)
|
||||
server_instance.ws.add_route("loader/call_legacy_plugin_method", self.handle_plugin_method_call_legacy)
|
||||
|
||||
async def enable_reload_wait(self):
|
||||
if self.live_reload:
|
||||
await sleep(10)
|
||||
@@ -107,22 +106,27 @@ class Loader:
|
||||
self.watcher.disabled = False
|
||||
|
||||
async def handle_frontend_assets(self, request: web.Request):
|
||||
file = path.join(path.dirname(__file__), "..", "static", request.match_info["path"])
|
||||
|
||||
file = Path(__file__).parent.joinpath("static").joinpath(request.match_info["path"])
|
||||
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
|
||||
|
||||
async def handle_frontend_locales(self, request: web.Request):
|
||||
req_lang = request.match_info["path"]
|
||||
file = path.join(path.dirname(__file__), "..", "locales", req_lang)
|
||||
file = Path(__file__).parent.joinpath("locales").joinpath(req_lang)
|
||||
if exists(file):
|
||||
return web.FileResponse(file, headers={"Cache-Control": "no-cache", "Content-Type": "application/json"})
|
||||
else:
|
||||
self.logger.info(f"Language {req_lang} not available, returning an empty dictionary")
|
||||
return web.json_response(data={}, headers={"Cache-Control": "no-cache"})
|
||||
|
||||
async def get_plugins(self, request: web.Request):
|
||||
async def get_plugins(self):
|
||||
plugins = list(self.plugins.values())
|
||||
return web.json_response([{"name": str(i) if not i.legacy else "$LEGACY_"+str(i), "version": i.version} for i in plugins])
|
||||
return [{"name": str(i), "version": i.version, "load_type": i.load_type} for i in plugins]
|
||||
|
||||
async def handle_plugin_dist(self, request: web.Request):
|
||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||
file = path.join(self.plugin_path, plugin.plugin_directory, "dist", request.match_info["path"])
|
||||
|
||||
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
|
||||
|
||||
async def handle_plugin_frontend_assets(self, request: web.Request):
|
||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||
@@ -136,101 +140,72 @@ class Loader:
|
||||
with open(path.join(self.plugin_path, plugin.plugin_directory, "dist/index.js"), "r", encoding="utf-8") as bundle:
|
||||
return web.Response(text=bundle.read(), content_type="application/javascript")
|
||||
|
||||
def import_plugin(self, file: str, plugin_directory: str, refresh: bool | None = False, batch: bool | None = False):
|
||||
async def import_plugin(self, file: str, plugin_directory: str, refresh: bool | None = False, batch: bool | None = False):
|
||||
try:
|
||||
plugin = PluginWrapper(file, plugin_directory, self.plugin_path)
|
||||
async def plugin_emitted_event(event: str, args: Any):
|
||||
self.logger.debug(f"PLUGIN EMITTED EVENT: {event} with args {args}")
|
||||
await self.ws.emit(f"loader/plugin_event", {"plugin": plugin.name, "event": event, "args": args})
|
||||
|
||||
plugin = PluginWrapper(file, plugin_directory, self.plugin_path, plugin_emitted_event)
|
||||
if plugin.name in self.plugins:
|
||||
if not "debug" in plugin.flags and refresh:
|
||||
self.logger.info(f"Plugin {plugin.name} is already loaded and has requested to not be re-loaded")
|
||||
return
|
||||
else:
|
||||
self.plugins[plugin.name].stop()
|
||||
await self.plugins[plugin.name].stop()
|
||||
self.plugins.pop(plugin.name, None)
|
||||
if plugin.passive:
|
||||
self.logger.info(f"Plugin {plugin.name} is passive")
|
||||
|
||||
self.plugins[plugin.name] = plugin.start()
|
||||
self.logger.info(f"Loaded {plugin.name}")
|
||||
if not batch:
|
||||
self.loop.create_task(self.dispatch_plugin(plugin.name if not plugin.legacy else "$LEGACY_" + plugin.name, plugin.version))
|
||||
self.loop.create_task(self.dispatch_plugin(plugin.name, plugin.version, plugin.load_type))
|
||||
except Exception as e:
|
||||
self.logger.error(f"Could not load {file}. {e}")
|
||||
print_exc()
|
||||
|
||||
async def dispatch_plugin(self, name: str, version: str | None):
|
||||
gpui_tab = await get_gamepadui_tab()
|
||||
await gpui_tab.evaluate_js(f"window.importDeckyPlugin('{name}', '{version}')")
|
||||
async def dispatch_plugin(self, name: str, version: str | None, load_type: int = PluginLoadType.ESMODULE_V1.value):
|
||||
await self.ws.emit("loader/import_plugin", name, version, load_type)
|
||||
|
||||
def import_plugins(self):
|
||||
async def import_plugins(self):
|
||||
self.logger.info(f"import plugins from {self.plugin_path}")
|
||||
|
||||
directories = [i for i in listdir(self.plugin_path) if path.isdir(path.join(self.plugin_path, i)) and path.isfile(path.join(self.plugin_path, i, "plugin.json"))]
|
||||
for directory in directories:
|
||||
self.logger.info(f"found plugin: {directory}")
|
||||
self.import_plugin(path.join(self.plugin_path, directory, "main.py"), directory, False, True)
|
||||
await self.import_plugin(path.join(self.plugin_path, directory, "main.py"), directory, False, True)
|
||||
|
||||
async def handle_reloads(self):
|
||||
while True:
|
||||
args = await self.reload_queue.get()
|
||||
self.import_plugin(*args) # type: ignore
|
||||
await self.import_plugin(*args) # pyright: ignore [reportArgumentType]
|
||||
|
||||
async def handle_plugin_method_call(self, request: web.Request):
|
||||
res = {}
|
||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||
method_name = request.match_info["method_name"]
|
||||
try:
|
||||
method_info = await request.json()
|
||||
args: Any = method_info["args"]
|
||||
except JSONDecodeError:
|
||||
args = {}
|
||||
async def handle_plugin_method_call_legacy(self, plugin_name: str, method_name: str, kwargs: Dict[Any, Any]):
|
||||
res: Dict[Any, Any] = {}
|
||||
plugin = self.plugins[plugin_name]
|
||||
try:
|
||||
if method_name.startswith("_"):
|
||||
raise RuntimeError("Tried to call private method")
|
||||
res["result"] = await plugin.execute_method(method_name, args)
|
||||
raise RuntimeError(f"Plugin {plugin.name} tried to call private method {method_name}")
|
||||
res["result"] = await plugin.execute_legacy_method(method_name, kwargs)
|
||||
res["success"] = True
|
||||
except Exception as e:
|
||||
res["result"] = str(e)
|
||||
res["success"] = False
|
||||
return web.json_response(res)
|
||||
return res
|
||||
|
||||
"""
|
||||
The following methods are used to load legacy plugins, which are considered deprecated.
|
||||
I made the choice to re-add them so that the first iteration/version of the react loader
|
||||
can work as a drop-in replacement for the stable branch of the PluginLoader, so that we
|
||||
can introduce it more smoothly and give people the chance to sample the new features even
|
||||
without plugin support. They will be removed once legacy plugins are no longer relevant.
|
||||
"""
|
||||
async def load_plugin_main_view(self, request: web.Request):
|
||||
plugin = self.plugins[request.match_info["name"]]
|
||||
with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.main_view_html), "r", encoding="utf-8") as template:
|
||||
template_data = template.read()
|
||||
ret = f"""
|
||||
<script src="/legacy/library.js"></script>
|
||||
<script>window.plugin_name = '{plugin.name}' </script>
|
||||
<base href="http://127.0.0.1:1337/plugins/plugin_resource/{plugin.name}/">
|
||||
{template_data}
|
||||
"""
|
||||
return web.Response(text=ret, content_type="text/html")
|
||||
|
||||
async def handle_sub_route(self, request: web.Request):
|
||||
plugin = self.plugins[request.match_info["name"]]
|
||||
route_path = request.match_info["path"]
|
||||
self.logger.info(path)
|
||||
ret = ""
|
||||
file_path = path.join(self.plugin_path, plugin.plugin_directory, route_path)
|
||||
with open(file_path, "r", encoding="utf-8") as resource_data:
|
||||
ret = resource_data.read()
|
||||
|
||||
return web.Response(text=ret)
|
||||
|
||||
async def get_steam_resource(self, request: web.Request):
|
||||
tab = await get_tab("SP")
|
||||
async def handle_plugin_method_call(self, plugin_name: str, method_name: str, *args: List[Any]):
|
||||
plugin = self.plugins[plugin_name]
|
||||
try:
|
||||
return web.Response(text=await tab.get_steam_resource(f"https://steamloopback.host/{request.match_info['path']}"), content_type="text/html")
|
||||
if method_name.startswith("_"):
|
||||
raise RuntimeError(f"Plugin {plugin.name} tried to call private method {method_name}")
|
||||
result = await plugin.execute_method(method_name, *args)
|
||||
except Exception as e:
|
||||
return web.Response(text=str(e), status=400)
|
||||
self.logger.error(f"Method {method_name} of plugin {plugin.name} failed with the following exception:\n{format_exc()}")
|
||||
raise e # throw again to pass the error to the frontend
|
||||
return result
|
||||
|
||||
async def handle_backend_reload_request(self, request: web.Request):
|
||||
plugin_name : str = request.match_info["plugin_name"]
|
||||
async def handle_plugin_backend_reload(self, plugin_name: str):
|
||||
plugin = self.plugins[plugin_name]
|
||||
|
||||
await self.reload_queue.put((plugin.file, plugin.plugin_directory))
|
||||
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"FilePickerIndex": {
|
||||
"files": {
|
||||
"file_type": "نوع الملف",
|
||||
"show_hidden": "أظهر الملفات المخفية",
|
||||
"all_files": "جميع الملفات"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "المنشئة (الأقدم)",
|
||||
"modified_asce": "المعدلة (الأقدم)",
|
||||
"modified_desc": "المعدلة (الأحدث)",
|
||||
"name_asce": "أ-ي",
|
||||
"name_desc": "أ-ي",
|
||||
"size_asce": "الحجم ( الأصغر)",
|
||||
"size_desc": "الحجم ( الأكبر)",
|
||||
"created_desc": "المنشئة (الأحدث)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "المجلد",
|
||||
"show_more": "أظهر المزيد من الملفات",
|
||||
"select": "إستخدم هذا المجلد"
|
||||
},
|
||||
"file": {
|
||||
"select": "إختر هذا الملف"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "هل أنت متأكد من التعديلات التالية؟",
|
||||
"description": {
|
||||
"reinstall": "إعادة تنصيب {{name}} {{version}}",
|
||||
"update": "تحديث {{name}} إلى {{version}}",
|
||||
"install": "تنصيب {{name}} {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "تأكيد"
|
||||
}
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "الخلف"
|
||||
}
|
||||
},
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"label": "قناة التحديثات",
|
||||
"prerelease": "الإصدار التجريبي",
|
||||
"stable": "إصدار مستقر",
|
||||
"testing": "إصدار تحت الإختبار"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "هذه الإضافة لديها الصلاحية للوصول لمحتويات Steam Deck.",
|
||||
"plugin_install": "تنصيب",
|
||||
"plugin_version_label": "رقم إصدار الإضافة",
|
||||
"plugin_no_desc": "لا يوجد وصف متاح."
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "تنصيب",
|
||||
"button_processing": "يتم التنصيب",
|
||||
"title": "تنصيب {{artifact}}",
|
||||
"desc": "هل أنت متأكد من رغبتك في تنصيب {{artifact}} {{version}}؟"
|
||||
},
|
||||
"reinstall": {
|
||||
"button_idle": "إعادة تنصيب",
|
||||
"button_processing": "تتم إعادة التنصيب",
|
||||
"desc": "هل أنت متأكد من رغبتك في إعادة تنصيب {{artifact}} {{version}}؟",
|
||||
"title": "إعادة تنصيب {{artifact}}"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "تحديث",
|
||||
"button_processing": "يتم التحديث",
|
||||
"title": "تحديث {{artifact}}",
|
||||
"desc": "هل أنت متأكد من رغبتك في تحديث {{artifact}} {{version}}؟"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"hide": "إخفاء من قائمة الوصول السريع",
|
||||
"reinstall": "إعادة التنصيب",
|
||||
"reload": "إعادة التحميل",
|
||||
"show": "إظهار في قائمة الوصول السريع",
|
||||
"unfreeze": "السماح بالتحديثات",
|
||||
"uninstall": "إزالة التنصيب",
|
||||
"update_to": "التحديث إلى {{name}}"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "مخفي من قائمة الوصول السريع"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"error": "خطا",
|
||||
"plugin_uninstall": {
|
||||
"button": "إزالة التنصيب",
|
||||
"title": "إزالة التنصيب {{name}}"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"react_devtools": {
|
||||
"ip_label": "عنوان الشبكة"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_zip": "تصفح",
|
||||
"button_install": "تنصيب"
|
||||
},
|
||||
"header": "أخرى"
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "إعادة التحميل في 5 ثواني"
|
||||
}
|
||||
}
|
||||
@@ -109,7 +109,9 @@
|
||||
"hide": "Rychlý přístup: Skrýt",
|
||||
"update_all_one": "Aktualizovat 1 plugin",
|
||||
"update_all_few": "Aktualizovat {{count}} pluginů",
|
||||
"update_all_other": "Aktualizovat {{count}} pluginů"
|
||||
"update_all_other": "Aktualizovat {{count}} pluginů",
|
||||
"freeze": "Pozastavit aktualizace",
|
||||
"unfreeze": "Povolit aktualizace"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
@@ -187,7 +189,8 @@
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Vývojář",
|
||||
"general_title": "Obecné",
|
||||
"plugins_title": "Pluginy"
|
||||
"plugins_title": "Pluginy",
|
||||
"testing_title": "Testování"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
@@ -213,7 +216,11 @@
|
||||
"about": "O Decky Plugin Store",
|
||||
"alph_asce": "Abecedně (Z do A)",
|
||||
"alph_desc": "Abecedně (A do Z)",
|
||||
"title": "Procházet"
|
||||
"title": "Procházet",
|
||||
"date_asce": "Nejstarší",
|
||||
"downloads_desc": "Nejvíce stažené",
|
||||
"date_desc": "Nejnovější",
|
||||
"downloads_asce": "Nejméně stažené"
|
||||
},
|
||||
"store_testing_cta": "Zvažte prosím testování nových pluginů, pomůžete tím týmu Decky Loader!",
|
||||
"store_testing_warning": {
|
||||
@@ -263,5 +270,8 @@
|
||||
"TitleView": {
|
||||
"settings_desc": "Otevřít nastavení Decky",
|
||||
"decky_store_desc": "Otevřít obchod Decky"
|
||||
},
|
||||
"Testing": {
|
||||
"download": "Stáhnout"
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,33 @@
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"disabling": "Deaktiviere",
|
||||
"enabling": "Aktiviere",
|
||||
"disabling": "Deaktiviere React DevTools",
|
||||
"enabling": "Aktiviere React DevTools",
|
||||
"5secreload": "Neu laden in 5 Sekunden"
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"folder": {
|
||||
"select": "Diesen Ordner verwenden"
|
||||
"select": "Diesen Ordner verwenden",
|
||||
"label": "Ordner",
|
||||
"show_more": "Mehr Dateien anzeigen"
|
||||
},
|
||||
"filter": {
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "Größe (Kleinste)",
|
||||
"size_desc": "Größe (Größte)",
|
||||
"created_asce": "Erstellt (Älteste)",
|
||||
"created_desc": "Erstellt (Neuste)",
|
||||
"modified_asce": "Geändert (Älteste)",
|
||||
"modified_desc": "Geändert (Neuste)",
|
||||
"name_asce": "Z-A"
|
||||
},
|
||||
"file": {
|
||||
"select": "Diese Datei auswählen"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "Alle Dateien",
|
||||
"file_type": "Dateityp",
|
||||
"show_hidden": "Versteckte Dateien anzeigen"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
@@ -51,8 +71,12 @@
|
||||
"reload": "Neu laden",
|
||||
"uninstall": "Deinstallieren",
|
||||
"update_to": "Aktualisieren zu {{name}}",
|
||||
"update_all_one": "",
|
||||
"update_all_other": ""
|
||||
"update_all_one": "{{count}} Plugin aktualisieren",
|
||||
"update_all_other": "{{count}} Plugins aktualisieren",
|
||||
"show": "Schnellzugriff: Anzeigen",
|
||||
"hide": "Schnellzugriff: Ausblenden",
|
||||
"freeze": "Updates einfrieren",
|
||||
"unfreeze": "Updates erlauben"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
@@ -96,6 +120,11 @@
|
||||
"desc2": "Fasse in diesem Menü nichts an, es sei denn, du weißt was du tust.",
|
||||
"label": "Aktiviere Valve-internes Menü",
|
||||
"desc1": "Aktiviert das Valve-interne Entwickler Menü."
|
||||
},
|
||||
"cef_console": {
|
||||
"button": "Konsole öffnen",
|
||||
"label": "CEF Konsole",
|
||||
"desc": "Öffnet die CEF Konsole. Nur für Debugzwecke. Dinge hier sind potentiell gefährlich und sollten nur durch oder unter Anleitung von Pluginentwickler/innen geschehen."
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
@@ -114,12 +143,18 @@
|
||||
},
|
||||
"updates": {
|
||||
"header": "Aktualisierungen"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Decky Update verfügbar",
|
||||
"header": "Benachrichtigungen",
|
||||
"plugin_updates_label": "Plugin Updates verfügbar"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Entwickler",
|
||||
"general_title": "Allgemein",
|
||||
"plugins_title": "Erweiterungen"
|
||||
"plugins_title": "Erweiterungen",
|
||||
"testing_title": "Testen"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
@@ -145,19 +180,27 @@
|
||||
"about": "Über",
|
||||
"alph_asce": "Alphabetisch (Z zu A)",
|
||||
"alph_desc": "Alphabetisch (A zu Z)",
|
||||
"title": "Durchstöbern"
|
||||
"title": "Durchstöbern",
|
||||
"date_desc": "Neuste Zuerst",
|
||||
"downloads_asce": "Wenigste Downloads Zuerst",
|
||||
"downloads_desc": "Meiste Downloads Zuerst",
|
||||
"date_asce": "Älteste Zuerst"
|
||||
},
|
||||
"store_testing_cta": "Unterstütze das Decky Loader Team mit dem Testen von neuen Erweiterungen!"
|
||||
"store_testing_cta": "Unterstütze das Decky Loader Team mit dem Testen von neuen Erweiterungen!",
|
||||
"store_testing_warning": {
|
||||
"label": "Willkommen zum Test Store Kanal",
|
||||
"desc": "Du kannst diesen Store Kanal nutzen, um brandneue Testversionen von Plugins auszuprobieren. Denk daran Feedback auf GitHub zu hinterlassen, sodass das Plugin für alle Nutzer verbessert werden kann."
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"label": "Benutzerdefinierter Marktplatz",
|
||||
"label": "Benutzerdefiniertes Store",
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Benutzerdefiniert",
|
||||
"default": "Standard",
|
||||
"label": "Marktplatz Kanal",
|
||||
"label": "Store Kanal",
|
||||
"testing": "Test"
|
||||
}
|
||||
},
|
||||
@@ -177,19 +220,51 @@
|
||||
"no_patch_notes_desc": "Für diese Version gibt es keine Patchnotizen"
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "",
|
||||
"hidden_other": ""
|
||||
"hidden_one": "{{count}} Plugin ist in dieser Liste ausgeblendet",
|
||||
"hidden_other": "{{count}} Plugins sind in dieser Liste ausgeblendet"
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"title": {
|
||||
"install_one": "",
|
||||
"install_other": "",
|
||||
"mixed_one": "",
|
||||
"mixed_other": "",
|
||||
"update_one": "",
|
||||
"update_other": "",
|
||||
"reinstall_one": "",
|
||||
"reinstall_other": ""
|
||||
"install_one": "{{count}} Plugin installieren",
|
||||
"install_other": "{{count}} Plugins installieren",
|
||||
"mixed_one": "{{count}} Plugin bearbeiten",
|
||||
"mixed_other": "{{count}} Plugins bearbeiten",
|
||||
"update_one": "{{count}} Plugin aktualisieren",
|
||||
"update_other": "{{count}} Plugins aktualisieren",
|
||||
"reinstall_one": "{{count}} Plugin neu installieren",
|
||||
"reinstall_other": "{{count}} Plugins neu installieren"
|
||||
},
|
||||
"description": {
|
||||
"install": "{{name}} {{version}} installieren",
|
||||
"update": "{{name}} auf {{version}} aktualisieren",
|
||||
"reinstall": "{{name}} {{version}} neu installieren"
|
||||
},
|
||||
"confirm": "Bist du sicher, dass du die folgenden Änderungen vornehmen möchtest?",
|
||||
"ok_button": {
|
||||
"loading": "An der Arbeit",
|
||||
"idle": "Bestätigen"
|
||||
}
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Im Schnellzugriff-Menu ausgeblendet"
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Decky Store Öffnen",
|
||||
"settings_desc": "Decky Einstellungen Öffnen"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Zurück"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"unknown": "Ein unbekannter Fehler ist aufgetreten. Die ursprüngliche Fehlermeldung ist: {{raw_error}}",
|
||||
"file_not_found": "Der Pfad ist ungültig. Bitte prüfen und erneut eingeben.",
|
||||
"perm_denied": "Kein Zugriff auf den angegebenen Dateipfad. Bitte prüfen, ob der Nutzer (deck auf dem Steam Deck) die entsprechenden Zugriffsrechte auf den angegebenen Ordner/die angegebene Datei hat."
|
||||
}
|
||||
},
|
||||
"Testing": {
|
||||
"download": "Download"
|
||||
}
|
||||
}
|
||||
@@ -231,6 +231,15 @@
|
||||
"store_testing_warning": {
|
||||
"desc": "You can use this store channel to test bleeding-edge plugin versions. Be sure to leave feedback on GitHub so the plugin can be updated for all users.",
|
||||
"label": "Welcome to the Testing Store Channel"
|
||||
},
|
||||
"download_progress_info": {
|
||||
"start": "Initializing",
|
||||
"open_zip": "Opening zip file",
|
||||
"download_zip": "Downloading plugin",
|
||||
"increment_count": "Incrementing download count",
|
||||
"parse_zip": "Parsing zip file",
|
||||
"uninstalling_previous": "Uninstalling previous copy",
|
||||
"installing_plugin": "Installing plugin"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
@@ -265,6 +274,10 @@
|
||||
}
|
||||
},
|
||||
"Testing": {
|
||||
"download": "Download"
|
||||
"download": "Download",
|
||||
"header": "The following versions of Decky Loader are built from open third-party Pull Requests. The Decky Loader team has not verified their functionality or security, and they may be outdated.",
|
||||
"loading": "Loading open Pull Requests...",
|
||||
"error": "Error Installing PR",
|
||||
"start_download_toast": "Downloading PR #{{id}}"
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,13 @@
|
||||
"desc1": "Active le menu développeur interne de Valve.",
|
||||
"desc2": "Ne touchez à rien dans ce menu à moins que vous ne sachiez ce qu'il fait.",
|
||||
"label": "Activer Valve Internal"
|
||||
}
|
||||
},
|
||||
"cef_console": {
|
||||
"button": "Ouvrir la console",
|
||||
"label": "CEF Console",
|
||||
"desc": "Ouvre la console CEF. Utile uniquement à des fins de débogage. Les éléments présentés ici sont potentiellement dangereux et ne doivent être utilisés que si vous êtes un développeur de plugins ou si vous êtes dirigé ici par un de ces développeurs."
|
||||
},
|
||||
"header": "Autre"
|
||||
},
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
@@ -122,7 +128,9 @@
|
||||
"update_all_many": "Mettre à jour {{count}} plugins",
|
||||
"update_all_other": "Mettre à jour {{count}} plugins",
|
||||
"show": "Accès Rapide : Afficher",
|
||||
"hide": "Accès rapide : Cacher"
|
||||
"hide": "Accès rapide : Cacher",
|
||||
"unfreeze": "Autoriser les mises à jour",
|
||||
"freeze": "Geler les mises à jour"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
@@ -164,12 +172,18 @@
|
||||
},
|
||||
"updates": {
|
||||
"header": "Mises à jour"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Mise à jour Decky disponible",
|
||||
"header": "Notifications",
|
||||
"plugin_updates_label": "Mises à jour du plugin disponibles"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Développeur",
|
||||
"general_title": "Général",
|
||||
"plugins_title": "Plugins"
|
||||
"plugins_title": "Plugins",
|
||||
"testing_title": "Essai"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
@@ -195,14 +209,22 @@
|
||||
"about": "À propos",
|
||||
"alph_asce": "Alphabétique (Z à A)",
|
||||
"alph_desc": "Alphabétique (A à Z)",
|
||||
"title": "Explorer"
|
||||
"title": "Explorer",
|
||||
"date_asce": "Plus ancien en premier",
|
||||
"date_desc": "Le plus récent d'abord",
|
||||
"downloads_asce": "Le moins téléchargé en premier",
|
||||
"downloads_desc": "Les plus téléchargés en premier"
|
||||
},
|
||||
"store_testing_cta": "Pensez à tester de nouveaux plugins pour aider l'équipe Decky Loader !"
|
||||
"store_testing_cta": "Pensez à tester de nouveaux plugins pour aider l'équipe Decky Loader !",
|
||||
"store_testing_warning": {
|
||||
"label": "Bienvenue sur la chaîne du magasin de tests",
|
||||
"desc": "Vous pouvez utiliser cette chaîne de magasin pour tester des versions de plugins. Assurez-vous de laisser des commentaires sur GitHub afin que le plugin puisse être mis à jour pour tous les utilisateurs."
|
||||
}
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "",
|
||||
"hidden_many": "",
|
||||
"hidden_other": ""
|
||||
"hidden_one": "1 plugin est masqué dans cette liste",
|
||||
"hidden_many": "{{count}} plugins sont masqués de cette liste",
|
||||
"hidden_other": "{{count}} plugins sont masqués de cette liste"
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"title": {
|
||||
@@ -244,5 +266,12 @@
|
||||
"button": {
|
||||
"back": "Retour"
|
||||
}
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Ouvrir le magasin Decky",
|
||||
"settings_desc": "Ouvrir les paramètres de Decky"
|
||||
},
|
||||
"Testing": {
|
||||
"download": "Télécharger"
|
||||
}
|
||||
}
|
||||
@@ -97,7 +97,9 @@
|
||||
"hide": "빠른 액세스 메뉴: 숨김",
|
||||
"update_all_other": "플러그인 {{count}}개 업데이트",
|
||||
"no_plugin": "설치된 플러그인이 없습니다!",
|
||||
"update_to": "{{name}}(으)로 업데이트"
|
||||
"update_to": "{{name}}(으)로 업데이트",
|
||||
"freeze": "업데이트 일시 중지",
|
||||
"unfreeze": "업데이트 허용"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
@@ -173,7 +175,8 @@
|
||||
"SettingsIndex": {
|
||||
"developer_title": "개발자",
|
||||
"general_title": "일반",
|
||||
"plugins_title": "플러그인"
|
||||
"plugins_title": "플러그인",
|
||||
"testing_title": "테스트"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
@@ -199,7 +202,11 @@
|
||||
"about": "정보",
|
||||
"alph_asce": "알파벳순 (Z-A)",
|
||||
"alph_desc": "알파벳순 (A-Z)",
|
||||
"title": "검색"
|
||||
"title": "검색",
|
||||
"downloads_asce": "다운로드 수 낮은 순",
|
||||
"date_desc": "최신 순",
|
||||
"date_asce": "오래된 순",
|
||||
"downloads_desc": "다운로드 많은 순"
|
||||
},
|
||||
"store_testing_cta": "새로운 플러그인을 테스트하여 Decky Loader 팀을 도와주세요!",
|
||||
"store_testing_warning": {
|
||||
@@ -249,5 +256,8 @@
|
||||
"TitleView": {
|
||||
"settings_desc": "Decky 설정 열기",
|
||||
"decky_store_desc": "Decky 스토어 열기"
|
||||
},
|
||||
"Testing": {
|
||||
"download": "다운로드"
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,9 @@
|
||||
"update_all_one": "Werk 1 plug-in bij",
|
||||
"update_all_other": "Werk {{count}} plug-ins bij",
|
||||
"reinstall": "Opnieuw installeren",
|
||||
"show": "Toon in snelle toegang"
|
||||
"show": "Toon in snelle toegang",
|
||||
"unfreeze": "Updates toestaan",
|
||||
"freeze": "Updates bevriezen"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
@@ -192,7 +194,8 @@
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Ontwikkelaar",
|
||||
"general_title": "Algemeen",
|
||||
"plugins_title": "Plug-ins"
|
||||
"plugins_title": "Plug-ins",
|
||||
"testing_title": "Testen"
|
||||
},
|
||||
"Store": {
|
||||
"store_filter": {
|
||||
@@ -214,7 +217,11 @@
|
||||
"about": "Over",
|
||||
"alph_asce": "Alfabetisch (Z naar A)",
|
||||
"alph_desc": "Alfabetisch (A naar Z)",
|
||||
"title": "Bladeren"
|
||||
"title": "Bladeren",
|
||||
"date_desc": "Nieuwste eerst",
|
||||
"downloads_asce": "Minste gedownload eerst",
|
||||
"downloads_desc": "Meeste gedownload eerst",
|
||||
"date_asce": "Oudste eerst"
|
||||
},
|
||||
"store_testing_cta": "Overweeg om nieuwe plug-ins te testen om het Decky Loader-team te helpen!",
|
||||
"store_contrib": {
|
||||
@@ -256,5 +263,8 @@
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Decky Store openen",
|
||||
"settings_desc": "Decky-instellingen openen"
|
||||
},
|
||||
"Testing": {
|
||||
"download": "Downloaden"
|
||||
}
|
||||
}
|
||||
@@ -104,7 +104,8 @@
|
||||
"update_all_one": "Atualizar 1 plugin",
|
||||
"update_all_many": "Atualizar {{count}} plugins",
|
||||
"update_all_other": "Atualizar {{count}} plugins",
|
||||
"hide": "Acesso Rápido: Ocultar"
|
||||
"hide": "Acesso Rápido: Ocultar",
|
||||
"freeze": "Congelar updates"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
@@ -36,7 +36,9 @@
|
||||
"show": "Быстрый доступ: Показать",
|
||||
"plugin_actions": "Действия с плагинами",
|
||||
"no_plugin": "Не установлено ни одного плагина!",
|
||||
"reinstall": "Переустановить"
|
||||
"reinstall": "Переустановить",
|
||||
"freeze": "Остановить обновления",
|
||||
"unfreeze": "Разрешить обновления"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"plugin_update_one": "Обновления доступны для {{count}} плагина!",
|
||||
@@ -184,7 +186,11 @@
|
||||
"about": "Информация",
|
||||
"alph_desc": "По алфавиту (A - Z)",
|
||||
"title": "Обзор",
|
||||
"alph_asce": "По алфавиту (Z - A)"
|
||||
"alph_asce": "По алфавиту (Z - A)",
|
||||
"date_asce": "Сначала старые",
|
||||
"date_desc": "Сначала новые",
|
||||
"downloads_asce": "Наименее загружаемые сначала",
|
||||
"downloads_desc": "Наиболее загружаемые сначала"
|
||||
},
|
||||
"store_testing_cta": "Пожалуйста, рассмотрите возможность тестирования новых плагинов, чтобы помочь команде Decky Loader!",
|
||||
"store_contrib": {
|
||||
@@ -258,10 +264,14 @@
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Разработчик",
|
||||
"general_title": "Общее",
|
||||
"plugins_title": "Плагины"
|
||||
"plugins_title": "Плагины",
|
||||
"testing_title": "Тестирование"
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Открыть магазин Decky",
|
||||
"settings_desc": "Открыть настройки Decky"
|
||||
},
|
||||
"Testing": {
|
||||
"download": "Загрузить"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"prerelease": "Förhandsversion",
|
||||
"stable": "Stabil",
|
||||
"testing": "Testning",
|
||||
"label": "Uppdateringskanal"
|
||||
}
|
||||
},
|
||||
"Developer": {
|
||||
"5secreload": "Omladdning på 5 sekunder",
|
||||
"disabling": "Inaktivera React DevTools",
|
||||
"enabling": "Aktivera React DevTools"
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Tillbaka"
|
||||
}
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "Den angivna sökvägen är inte giltig. Kontrollera den och ange den korrekt igen.",
|
||||
"unknown": "Ett okänt fel har inträffat. Det råa felet är: {{raw_error}}",
|
||||
"perm_denied": "Du har inte tillgång till den angivna katalogen. Kontrollera om din användare (deck på Steam Deck) har motsvarande behörighet för att komma åt den angivna mappen/filen."
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "Välj denna fil"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "Alla Filer",
|
||||
"file_type": "Filtyp",
|
||||
"show_hidden": "Visa dolda filer"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "Skapad (Äldst)",
|
||||
"created_desc": "Skapad (nyast)",
|
||||
"modified_asce": "Modifierad (Äldst)",
|
||||
"modified_desc": "Modifierad (nyaste)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "Storlek (minst)",
|
||||
"size_desc": "Storlek (Störst)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "Mapp",
|
||||
"select": "Använd denna mapp",
|
||||
"show_more": "Visa fler filer"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"description": {
|
||||
"install": "Installera {{name}} {{version}}",
|
||||
"reinstall": "Installera om {{name}} {{version}}",
|
||||
"update": "Uppdatera {{name}} {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Bekräfta",
|
||||
"loading": "Arbetar"
|
||||
},
|
||||
"title": {
|
||||
"install_one": "Install 1 tillägg",
|
||||
"install_other": "Installerar {{count}} tillägg"
|
||||
},
|
||||
"confirm": "Är du säker på att du vill göra följande ändringar?"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
{
|
||||
"BranchSelect": {
|
||||
"update_channel": {
|
||||
"prerelease": "Önsürüm",
|
||||
"stable": "Stabil",
|
||||
"testing": "Test",
|
||||
"label": "Güncelleme Kanalı"
|
||||
}
|
||||
},
|
||||
"DropdownMultiselect": {
|
||||
"button": {
|
||||
"back": "Geri"
|
||||
}
|
||||
},
|
||||
"FilePickerIndex": {
|
||||
"file": {
|
||||
"select": "Bu dosyayı seçin"
|
||||
},
|
||||
"files": {
|
||||
"all_files": "Tüm Dosyalar",
|
||||
"file_type": "Dosya Türü",
|
||||
"show_hidden": "Gizli Dosyaları Göster"
|
||||
},
|
||||
"filter": {
|
||||
"created_asce": "Oluşturuldu (En Eski)",
|
||||
"created_desc": "Oluşturuldu (En Yeni)",
|
||||
"modified_asce": "Değiştirildi (En Eski)",
|
||||
"modified_desc": "Değiştirildi (En Yeni)",
|
||||
"name_asce": "Z-A",
|
||||
"name_desc": "A-Z",
|
||||
"size_asce": "Boyut (En Küçük)",
|
||||
"size_desc": "Boyut (En Büyük)"
|
||||
},
|
||||
"folder": {
|
||||
"label": "Klasör",
|
||||
"select": "Bu klasörü kullan",
|
||||
"show_more": "Daha fazla dosya göster"
|
||||
}
|
||||
},
|
||||
"MultiplePluginsInstallModal": {
|
||||
"confirm": "Aşağıdaki değişiklikleri yapmak istediğinizden emin misiniz?",
|
||||
"description": {
|
||||
"install": "Yükle {{name}} {{version}}",
|
||||
"reinstall": "Yeniden yükle {{name}} {{version}}"
|
||||
},
|
||||
"ok_button": {
|
||||
"idle": "Onayla",
|
||||
"loading": "Çalışıyor"
|
||||
},
|
||||
"title": {
|
||||
"reinstall_one": "1 eklentiyi yeniden yükle",
|
||||
"reinstall_other": "{{count}} eklentiyi yeniden yükle",
|
||||
"install_one": "1 eklenti yükle",
|
||||
"install_other": "{{count}} eklenti yükle"
|
||||
}
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_full_access": "Bu eklenti Steam Deck'inize tam erişime sahiptir.",
|
||||
"plugin_install": "Yükle",
|
||||
"plugin_version_label": "Eklenti Versiyonu"
|
||||
},
|
||||
"PluginInstallModal": {
|
||||
"install": {
|
||||
"button_idle": "Yükle",
|
||||
"button_processing": "Yükleniyor",
|
||||
"title": "Yükle {{artifact}}",
|
||||
"desc": "Yüklemek istediğinizden emin misiniz {{artifact}} {{version}}?"
|
||||
},
|
||||
"reinstall": {
|
||||
"button_idle": "Yeniden Yükle",
|
||||
"desc": "Yeniden yüklemek istediğinizden emin misiniz {{artifact}} {{version}}?",
|
||||
"title": "Yeniden Yükle {{artifact}}",
|
||||
"button_processing": "Yeniden Yükleniyor"
|
||||
},
|
||||
"update": {
|
||||
"button_idle": "Güncelle",
|
||||
"button_processing": "Güncelleniyor",
|
||||
"title": "Güncelle {{artifact}}",
|
||||
"desc": "Güncellemek istediğinizden emin misiniz {{artifact}} {{version}}?"
|
||||
}
|
||||
},
|
||||
"PluginListIndex": {
|
||||
"freeze": "Güncellemeleri durdur",
|
||||
"hide": "Hızlı erişim: Gizle",
|
||||
"no_plugin": "Yüklü eklenti yok!",
|
||||
"plugin_actions": "Eklenti İşlemleri",
|
||||
"reinstall": "Yeniden Yükle",
|
||||
"show": "Hızlı erişim: Göster",
|
||||
"unfreeze": "Güncellemelere izin ver",
|
||||
"uninstall": "Kaldır",
|
||||
"update_all_one": "1 eklentiyi güncelle",
|
||||
"update_all_other": "{{count}} eklentiyi güncelle"
|
||||
},
|
||||
"PluginListLabel": {
|
||||
"hidden": "Hızlı erişim menüsünden gizlenmiş"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
"decky_update_available": "{{tag_name}} güncellemesi mevcut!",
|
||||
"error": "Hata",
|
||||
"plugin_load_error": {
|
||||
"toast": "{{name}} yüklenirken hata oluştu",
|
||||
"message": "{{name}} eklentisi yüklenirken bir hata oluştu"
|
||||
},
|
||||
"plugin_uninstall": {
|
||||
"button": "Kaldır",
|
||||
"desc": "{{name}} kaldırmak istediğinizden emin misiniz?",
|
||||
"title": "Kaldır {{name}}"
|
||||
},
|
||||
"plugin_update_one": "1 eklenti için güncelleme mevcut!",
|
||||
"plugin_update_other": "{{count}} eklenti için güncelleme mevcut!"
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"cef_console": {
|
||||
"button": "Konsolu Aç"
|
||||
},
|
||||
"header": "Diğer",
|
||||
"react_devtools": {
|
||||
"ip_label": "IP"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "Yükle",
|
||||
"button_zip": "Gözat",
|
||||
"header": "Üçüncü Parti Eklentiler",
|
||||
"label_desc": "URL",
|
||||
"label_url": "URL'den Eklenti Yükle",
|
||||
"label_zip": "ZIP Dosyasından Eklenti Yükle"
|
||||
}
|
||||
},
|
||||
"SettingsGeneralIndex": {
|
||||
"about": {
|
||||
"decky_version": "Decky Versiyonu",
|
||||
"header": "Hakkında"
|
||||
},
|
||||
"beta": {
|
||||
"header": "Betaya katılım"
|
||||
},
|
||||
"developer_mode": {
|
||||
"label": "Geliştirici modu"
|
||||
},
|
||||
"notifications": {
|
||||
"decky_updates_label": "Decky güncellemesi mevcut",
|
||||
"header": "Bildirimler",
|
||||
"plugin_updates_label": "Eklenti güncellemesi mevcut"
|
||||
},
|
||||
"other": {
|
||||
"header": "Diğer"
|
||||
},
|
||||
"updates": {
|
||||
"header": "Güncellemeler"
|
||||
}
|
||||
},
|
||||
"SettingsIndex": {
|
||||
"developer_title": "Geliştirici",
|
||||
"general_title": "Genel",
|
||||
"plugins_title": "Eklentiler"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
"label": "Katkıda Bulunma"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "Filtre",
|
||||
"label_def": "Tümü"
|
||||
},
|
||||
"store_search": {
|
||||
"label": "Ara"
|
||||
},
|
||||
"store_sort": {
|
||||
"label": "Sırala",
|
||||
"label_def": "Son Güncellenme (En Yeni)"
|
||||
},
|
||||
"store_source": {
|
||||
"label": "Kaynak Kodu"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "Hakkında",
|
||||
"alph_asce": "Alfabetik (Z'den A'ya)",
|
||||
"alph_desc": "Alfabetik (A'dan Z'ye)",
|
||||
"date_asce": "Önce En Eski",
|
||||
"date_desc": "Önce En Yeni",
|
||||
"downloads_desc": "Önce En Çok İndirilen",
|
||||
"title": "Gözat",
|
||||
"downloads_asce": "Önce En Az İndirilen"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
"url_label": "URL"
|
||||
},
|
||||
"store_channel": {
|
||||
"custom": "Özel",
|
||||
"default": "Varsayılan"
|
||||
}
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "Decky Mağazasını Aç",
|
||||
"settings_desc": "Decky Ayarlarını Aç"
|
||||
},
|
||||
"Updater": {
|
||||
"decky_updates": "Decky Güncellemeleri",
|
||||
"patch_notes_desc": "Yama Notları",
|
||||
"no_patch_notes_desc": "bu sürüm için yama notları mevcut değil",
|
||||
"updates": {
|
||||
"check_button": "Güncellemeleri Kontrol Et",
|
||||
"checking": "Kontrol ediliyor",
|
||||
"cur_version": "Mevcut Versiyon: {{ver}}",
|
||||
"install_button": "Güncellemeyi Yükle",
|
||||
"label": "Güncellemeler",
|
||||
"updating": "Güncelleniyor"
|
||||
}
|
||||
},
|
||||
"Testing": {
|
||||
"download": "İndir"
|
||||
},
|
||||
"FilePickerError": {
|
||||
"errors": {
|
||||
"file_not_found": "Belirtilen yol geçerli değil. Lütfen yolu kontrol edin ve doğru şekilde yeniden girin."
|
||||
}
|
||||
},
|
||||
"PluginView": {
|
||||
"hidden_one": "1 eklenti bu listeden gizlenmiştir",
|
||||
"hidden_other": "{{count}} eklenti bu listeden gizlenmiştir"
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,9 @@
|
||||
"update_to": "更新 {{name}}",
|
||||
"update_all_other": "更新 {{count}} 个插件",
|
||||
"show": "在快速访问菜单中显示",
|
||||
"hide": "在快速访问菜单中隐藏"
|
||||
"hide": "在快速访问菜单中隐藏",
|
||||
"unfreeze": "允许更新",
|
||||
"freeze": "暂停更新"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
@@ -93,15 +95,15 @@
|
||||
},
|
||||
"RemoteDebugging": {
|
||||
"remote_cef": {
|
||||
"desc": "允许你网络中的任何人无需身份验证即可访问CEF调试器",
|
||||
"label": "允许远程访问CEF调试"
|
||||
"desc": "允许你的网络中的任何人无需身份验证即可访问 CEF 调试器",
|
||||
"label": "允许 CEF 远程调试"
|
||||
}
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"react_devtools": {
|
||||
"ip_label": "IP",
|
||||
"label": "启用 React DevTools",
|
||||
"desc": "允许连接到运行着 React DevTools 的计算机,更改此设置将重新加载Steam,请在启用前设置IP地址。"
|
||||
"desc": "允许连接到运行着 React DevTools 的计算机。更改此设置将重新加载 Steam。请在启用前设置 IP 地址。"
|
||||
},
|
||||
"third_party_plugins": {
|
||||
"button_install": "安装",
|
||||
@@ -149,12 +151,13 @@
|
||||
"SettingsIndex": {
|
||||
"developer_title": "开发者",
|
||||
"general_title": "通用",
|
||||
"plugins_title": "插件"
|
||||
"plugins_title": "插件",
|
||||
"testing_title": "测试"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
"label": "贡献",
|
||||
"desc": "如果你想要提交你的插件到 Decky 插件商店,请访问 GitHub 上的 SteamDeckHomebrew/decky-plugin-template 存储库,关于开发和分发的相关信息,请查看 README 文件。"
|
||||
"desc": "如果你想要提交你的插件到 Decky 插件商店,请访问 GitHub 上的 SteamDeckHomebrew/decky-plugin-template 存储库。有关开发和分发插件的信息,请查看 README 文件。"
|
||||
},
|
||||
"store_filter": {
|
||||
"label": "过滤器",
|
||||
@@ -169,13 +172,17 @@
|
||||
},
|
||||
"store_source": {
|
||||
"label": "源代码",
|
||||
"desc": "所有插件的源代码都可以在 GitHub 上的 SteamDeckHomebrew/decky-plugin-database 存储库中获得。"
|
||||
"desc": "所有插件的源代码都可从 GitHub 上的 SteamDeckHomebrew/decky-plugin-database 存储库中获得。"
|
||||
},
|
||||
"store_tabs": {
|
||||
"about": "关于",
|
||||
"alph_asce": "字母排序 (Z 到 A)",
|
||||
"alph_desc": "字母排序 (A 到 Z)",
|
||||
"title": "浏览"
|
||||
"title": "浏览",
|
||||
"downloads_desc": "下载量倒序",
|
||||
"date_asce": "更新时间正序",
|
||||
"date_desc": "更新时间倒序",
|
||||
"downloads_asce": "下载量正序"
|
||||
},
|
||||
"store_testing_cta": "请考虑测试新插件以帮助 Decky Loader 团队!",
|
||||
"store_testing_warning": {
|
||||
@@ -243,11 +250,14 @@
|
||||
"errors": {
|
||||
"file_not_found": "指定路径无效。请检查并输入正确的路径。",
|
||||
"unknown": "发生了一个未知错误。原始错误为:{{raw_error}}",
|
||||
"perm_denied": "你没有访问特定目录的权限。请检查你的用户(Steam Deck 中的 deck 账户)有着相对应的权限以访问特定的文件夹或文件。"
|
||||
"perm_denied": "你没有访问特定目录的权限。请检查你的用户(Steam Deck 中的 deck 账户)是否有权访问特定的文件夹或文件。"
|
||||
}
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "打开 Decky 商店",
|
||||
"settings_desc": "打开 Decky 设置"
|
||||
},
|
||||
"Testing": {
|
||||
"download": "下载"
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@
|
||||
},
|
||||
"PluginCard": {
|
||||
"plugin_install": "安裝",
|
||||
"plugin_no_desc": "未提示描述。",
|
||||
"plugin_no_desc": "未提供描述。",
|
||||
"plugin_version_label": "外掛程式版本",
|
||||
"plugin_full_access": "此外掛程式擁有您的 Steam Deck 的完整存取權。"
|
||||
},
|
||||
@@ -73,7 +73,9 @@
|
||||
"reload": "重新載入",
|
||||
"show": "快速存取:顯示",
|
||||
"hide": "快速存取:隱藏",
|
||||
"update_all_other": "更新 {{count}} 個外掛程式"
|
||||
"update_all_other": "更新 {{count}} 個外掛程式",
|
||||
"freeze": "禁止更新",
|
||||
"unfreeze": "允許更新"
|
||||
},
|
||||
"PluginLoader": {
|
||||
"decky_title": "Decky",
|
||||
@@ -99,7 +101,7 @@
|
||||
},
|
||||
"SettingsDeveloperIndex": {
|
||||
"third_party_plugins": {
|
||||
"button_zip": "開啟",
|
||||
"button_zip": "瀏覽",
|
||||
"label_desc": "網址",
|
||||
"label_url": "從網址安裝外掛程式",
|
||||
"label_zip": "從 ZIP 檔案安裝外掛程式",
|
||||
@@ -149,7 +151,8 @@
|
||||
"SettingsIndex": {
|
||||
"developer_title": "開發人員",
|
||||
"general_title": "一般",
|
||||
"plugins_title": "外掛程式"
|
||||
"plugins_title": "外掛程式",
|
||||
"testing_title": "測試"
|
||||
},
|
||||
"Store": {
|
||||
"store_contrib": {
|
||||
@@ -175,9 +178,17 @@
|
||||
"about": "關於",
|
||||
"alph_asce": "依字母排序 (Z 到 A)",
|
||||
"alph_desc": "依字母排序 (A 到 Z)",
|
||||
"title": "瀏覽"
|
||||
"title": "瀏覽",
|
||||
"downloads_desc": "下載量高到低",
|
||||
"downloads_asce": "下載量低到高",
|
||||
"date_asce": "日期舊到新",
|
||||
"date_desc": "日期新到舊"
|
||||
},
|
||||
"store_testing_cta": "請考慮測試新的外掛程式來幫助 Decky Loader 團隊!"
|
||||
"store_testing_cta": "請考慮測試新的外掛程式來幫助 Decky Loader 團隊!",
|
||||
"store_testing_warning": {
|
||||
"label": "歡迎來到測試頻道",
|
||||
"desc": "您可以使用此商店頻道來體驗測試外掛版本。請務必在 GitHub 上留下回饋,以便為所有用戶更新該外掛程式。"
|
||||
}
|
||||
},
|
||||
"StoreSelect": {
|
||||
"custom_store": {
|
||||
@@ -226,7 +237,7 @@
|
||||
"confirm": "您確定要進行以下的修改嗎?",
|
||||
"description": {
|
||||
"install": "安裝 {{name}} {{version}}",
|
||||
"update": "更新 {{name}} 到 {{version}}",
|
||||
"update": "更新 {{name}} 的版本到 {{version}}",
|
||||
"reinstall": "重新安裝 {{name}} {{version}}"
|
||||
}
|
||||
},
|
||||
@@ -241,5 +252,12 @@
|
||||
"button": {
|
||||
"back": "返回"
|
||||
}
|
||||
},
|
||||
"TitleView": {
|
||||
"decky_store_desc": "開啟 Decky 商店",
|
||||
"settings_desc": "開啟 Decky 設定"
|
||||
},
|
||||
"Testing": {
|
||||
"download": "下載"
|
||||
}
|
||||
}
|
||||
+18
-3
@@ -1,6 +1,6 @@
|
||||
import os, pwd, grp, sys, logging
|
||||
from subprocess import call, run, DEVNULL, PIPE, STDOUT
|
||||
from .customtypes import UserType
|
||||
from ..enums import UserType
|
||||
|
||||
logger = logging.getLogger("localplatform")
|
||||
|
||||
@@ -156,12 +156,20 @@ async def service_start(service_name : str) -> bool:
|
||||
res = run(cmd, stdout=PIPE, stderr=STDOUT)
|
||||
return res.returncode == 0
|
||||
|
||||
async def restart_webhelper() -> bool:
|
||||
logger.info("Restarting steamwebhelper")
|
||||
# TODO move to pkill
|
||||
res = run(["killall", "-s", "SIGTERM", "steamwebhelper"], stdout=DEVNULL, stderr=DEVNULL)
|
||||
return res.returncode == 0
|
||||
|
||||
def get_privileged_path() -> str:
|
||||
path = os.getenv("PRIVILEGED_PATH")
|
||||
|
||||
if path == None:
|
||||
path = get_unprivileged_path()
|
||||
|
||||
os.makedirs(path, exist_ok=True)
|
||||
|
||||
return path
|
||||
|
||||
def _parent_dir(path : str | None) -> str | None:
|
||||
@@ -181,8 +189,13 @@ def get_unprivileged_path() -> str:
|
||||
|
||||
if path == None:
|
||||
logger.debug("Unprivileged path is not properly configured. Making something up!")
|
||||
# Expected path of loader binary is /home/deck/homebrew/service/PluginLoader
|
||||
path = _parent_dir(_parent_dir(os.path.realpath(sys.argv[0])))
|
||||
|
||||
if hasattr(sys, 'frozen'):
|
||||
# Expected path of loader binary is /home/deck/homebrew/service/PluginLoader
|
||||
path = _parent_dir(_parent_dir(os.path.realpath(sys.argv[0])))
|
||||
else:
|
||||
# Expected path of this file is $src_root/backend/src/localplatformlinux.py
|
||||
path = _parent_dir(_parent_dir(_parent_dir(__file__)))
|
||||
|
||||
if path != None and not os.path.exists(path):
|
||||
path = None
|
||||
@@ -191,6 +204,8 @@ def get_unprivileged_path() -> str:
|
||||
logger.warn("Unprivileged path is not properly configured. Defaulting to /home/deck/homebrew")
|
||||
path = "/home/deck/homebrew" # We give up
|
||||
|
||||
os.makedirs(path, exist_ok=True)
|
||||
|
||||
return path
|
||||
|
||||
|
||||
+6
-1
@@ -1,4 +1,4 @@
|
||||
from .customtypes import UserType
|
||||
from ..enums import UserType
|
||||
import os, sys
|
||||
|
||||
def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool = True) -> bool:
|
||||
@@ -47,7 +47,12 @@ def get_unprivileged_path() -> str:
|
||||
if path == None:
|
||||
path = os.getenv("PRIVILEGED_PATH", os.path.join(os.path.expanduser("~"), "homebrew"))
|
||||
|
||||
os.makedirs(path, exist_ok=True)
|
||||
|
||||
return path
|
||||
|
||||
def get_unprivileged_user() -> str:
|
||||
return os.getenv("UNPRIVILEGED_USER", os.getlogin())
|
||||
|
||||
async def restart_webhelper() -> bool:
|
||||
return True # Stubbed
|
||||
@@ -1,5 +1,5 @@
|
||||
import asyncio, time
|
||||
from typing import Awaitable, Callable
|
||||
from typing import Any, Callable, Coroutine
|
||||
import random
|
||||
|
||||
from .localplatform import ON_WINDOWS
|
||||
@@ -7,7 +7,7 @@ from .localplatform import ON_WINDOWS
|
||||
BUFFER_LIMIT = 2 ** 20 # 1 MiB
|
||||
|
||||
class UnixSocket:
|
||||
def __init__(self, on_new_message: Callable[[str], Awaitable[str|None]]):
|
||||
def __init__(self, on_new_message: Callable[[str], Coroutine[Any, Any, Any]]):
|
||||
'''
|
||||
on_new_message takes 1 string argument.
|
||||
It's return value gets used, if not None, to write data to the socket.
|
||||
@@ -18,6 +18,7 @@ class UnixSocket:
|
||||
self.socket = None
|
||||
self.reader = None
|
||||
self.writer = None
|
||||
self.server_writer = None
|
||||
|
||||
async def setup_server(self):
|
||||
self.socket = await asyncio.start_unix_server(self._listen_for_method_call, path=self.socket_addr, limit=BUFFER_LIMIT)
|
||||
@@ -74,7 +75,7 @@ class UnixSocket:
|
||||
try:
|
||||
line.extend(await reader.readuntil())
|
||||
except asyncio.LimitOverrunError:
|
||||
line.extend(await reader.read(reader._limit)) # type: ignore
|
||||
line.extend(await reader.read(reader._limit)) # pyright: ignore [reportUnknownMemberType, reportUnknownArgumentType, reportAttributeAccessIssue]
|
||||
continue
|
||||
except asyncio.IncompleteReadError as err:
|
||||
line.extend(err.partial)
|
||||
@@ -90,21 +91,26 @@ class UnixSocket:
|
||||
|
||||
writer.write(message.encode("utf-8"))
|
||||
await writer.drain()
|
||||
|
||||
async def write_single_line_server(self, message: str):
|
||||
if self.server_writer is None:
|
||||
return
|
||||
await self._write_single_line(self.server_writer, message)
|
||||
|
||||
async def _listen_for_method_call(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
|
||||
self.server_writer = writer
|
||||
while True:
|
||||
|
||||
def _(task: asyncio.Task[str|None]):
|
||||
res = task.result()
|
||||
if res is not None:
|
||||
asyncio.create_task(self._write_single_line(writer, res))
|
||||
|
||||
line = await self._read_single_line(reader)
|
||||
|
||||
try:
|
||||
res = await self.on_new_message(line)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
if res != None:
|
||||
await self._write_single_line(writer, res)
|
||||
asyncio.create_task(self.on_new_message(line)).add_done_callback(_)
|
||||
|
||||
class PortSocket (UnixSocket):
|
||||
def __init__(self, on_new_message: Callable[[str], Awaitable[str|None]]):
|
||||
def __init__(self, on_new_message: Callable[[str], Coroutine[Any, Any, Any]]):
|
||||
'''
|
||||
on_new_message takes 1 string argument.
|
||||
It's return value gets used, if not None, to write data to the socket.
|
||||
@@ -136,4 +142,4 @@ if ON_WINDOWS:
|
||||
pass
|
||||
else:
|
||||
class LocalSocket (UnixSocket):
|
||||
pass
|
||||
pass
|
||||
@@ -1,10 +1,10 @@
|
||||
# Change PyInstaller files permissions
|
||||
import sys
|
||||
from typing import Dict
|
||||
from .localplatform import (chmod, chown, service_stop, service_start,
|
||||
ON_WINDOWS, get_log_level, get_live_reload,
|
||||
from .localplatform.localplatform import (chmod, chown, service_stop, service_start,
|
||||
ON_WINDOWS, ON_LINUX, get_log_level, get_live_reload,
|
||||
get_server_port, get_server_host, get_chown_plugin_path,
|
||||
get_privileged_path)
|
||||
get_privileged_path, restart_webhelper)
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
chmod(sys._MEIPASS, 755) # type: ignore
|
||||
# Full imports
|
||||
@@ -14,23 +14,24 @@ from os import path
|
||||
from traceback import format_exc
|
||||
import multiprocessing
|
||||
|
||||
import aiohttp_cors # type: ignore
|
||||
import aiohttp_cors # pyright: ignore [reportMissingTypeStubs]
|
||||
# Partial imports
|
||||
from aiohttp import client_exceptions
|
||||
from aiohttp.web import Application, Response, Request, get, run_app, static # type: ignore
|
||||
from aiohttp.web import Application, Response, Request, get, run_app, static # pyright: ignore [reportUnknownVariableType]
|
||||
from aiohttp_jinja2 import setup as jinja_setup
|
||||
|
||||
# local modules
|
||||
from .browser import PluginBrowser
|
||||
from .helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token,
|
||||
from .helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token, get_loader_version,
|
||||
mkdir_as_user, get_system_pythonpaths, get_effective_user_id)
|
||||
|
||||
from .injector import get_gamepadui_tab, Tab, close_old_tabs
|
||||
from .injector import get_gamepadui_tab, Tab
|
||||
from .loader import Loader
|
||||
from .settings import SettingsManager
|
||||
from .updater import Updater
|
||||
from .utilities import Utilities
|
||||
from .customtypes import UserType
|
||||
from .enums import UserType
|
||||
from .wsrouter import WSRouter
|
||||
|
||||
|
||||
basicConfig(
|
||||
@@ -63,7 +64,8 @@ class PluginManager:
|
||||
allow_credentials=True
|
||||
)
|
||||
})
|
||||
self.plugin_loader = Loader(self, plugin_path, self.loop, get_live_reload())
|
||||
self.ws = WSRouter(self.loop, self.web_app)
|
||||
self.plugin_loader = Loader(self, self.ws, plugin_path, self.loop, get_live_reload())
|
||||
self.settings = SettingsManager("loader", path.join(get_privileged_path(), "settings"))
|
||||
self.plugin_browser = PluginBrowser(plugin_path, self.plugin_loader.plugins, self.plugin_loader, self.settings)
|
||||
self.utilities = Utilities(self)
|
||||
@@ -85,9 +87,8 @@ class PluginManager:
|
||||
self.web_app.add_routes([get("/auth/token", self.get_auth_token)])
|
||||
|
||||
for route in list(self.web_app.router.routes()):
|
||||
self.cors.add(route) # type: ignore
|
||||
self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), '..', 'static'))])
|
||||
self.web_app.add_routes([static("/legacy", path.join(path.dirname(__file__), 'legacy'))])
|
||||
self.cors.add(route) # pyright: ignore [reportUnknownMemberType]
|
||||
self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), 'static'))])
|
||||
|
||||
def exception_handler(self, loop: AbstractEventLoop, context: Dict[str, str]):
|
||||
if context["message"] == "Unclosed connection":
|
||||
@@ -100,8 +101,7 @@ class PluginManager:
|
||||
async def load_plugins(self):
|
||||
# await self.wait_for_server()
|
||||
logger.debug("Loading plugins")
|
||||
self.plugin_loader.import_plugins()
|
||||
# await inject_to_tab("SP", "window.syncDeckyPlugins();")
|
||||
await self.plugin_loader.import_plugins()
|
||||
if self.settings.getSetting("pluginOrder", None) == None:
|
||||
self.settings.setSetting("pluginOrder", list(self.plugin_loader.plugins.keys()))
|
||||
logger.debug("Did not find pluginOrder setting, set it to default")
|
||||
@@ -131,16 +131,13 @@ class PluginManager:
|
||||
await self.inject_javascript(tab, True)
|
||||
try:
|
||||
async for msg in tab.listen_for_message():
|
||||
# this gets spammed a lot
|
||||
if msg.get("method", None) != "Page.navigatedWithinDocument":
|
||||
logger.debug("Page event: " + str(msg.get("method", None)))
|
||||
if msg.get("method", None) == "Page.domContentEventFired":
|
||||
if not await tab.has_global_var("deckyHasLoaded", False):
|
||||
await self.inject_javascript(tab)
|
||||
if msg.get("method", None) == "Inspector.detached":
|
||||
logger.info("CEF has requested that we detach.")
|
||||
await tab.close_websocket()
|
||||
break
|
||||
if msg.get("method", None) == "Page.domContentEventFired":
|
||||
if not await tab.has_global_var("deckyHasLoaded", False):
|
||||
await self.inject_javascript(tab)
|
||||
elif msg.get("method", None) == "Inspector.detached":
|
||||
logger.info("CEF has requested that we detach.")
|
||||
await tab.close_websocket()
|
||||
break
|
||||
# If this is a forceful disconnect the loop will just stop without any failure message. In this case, injector.py will handle this for us so we don't need to close the socket.
|
||||
# This is because of https://github.com/aio-libs/aiohttp/blob/3ee7091b40a1bc58a8d7846e7878a77640e96996/aiohttp/client_ws.py#L321
|
||||
logger.info("CEF has disconnected...")
|
||||
@@ -158,10 +155,11 @@ class PluginManager:
|
||||
async def inject_javascript(self, tab: Tab, first: bool=False, request: Request|None=None):
|
||||
logger.info("Loading Decky frontend!")
|
||||
try:
|
||||
if first:
|
||||
if await tab.has_global_var("deckyHasLoaded", False):
|
||||
await close_old_tabs()
|
||||
await tab.evaluate_js("try{if (window.deckyHasLoaded){setTimeout(() => location.reload(), 100)}else{window.deckyHasLoaded = true;(async()=>{try{while(!window.SP_REACT){await new Promise(r => setTimeout(r, 10))};await import('http://localhost:1337/frontend/index.js')}catch(e){console.error(e)};})();}}catch(e){console.error(e)}", False, False, False)
|
||||
# if first:
|
||||
if ON_LINUX and await tab.has_global_var("deckyHasLoaded", False):
|
||||
await restart_webhelper()
|
||||
return # We'll catch the next tab in the main loop
|
||||
await tab.evaluate_js("try{if (window.deckyHasLoaded){setTimeout(() => SteamClient.Browser.RestartJSContext(), 100)}else{window.deckyHasLoaded = true;(async()=>{try{await import('http://localhost:1337/frontend/index.js?v=%s')}catch(e){console.error(e)};})();}}catch(e){console.error(e)}" % (get_loader_version(), ), False, False, False)
|
||||
except:
|
||||
logger.info("Failed to inject JavaScript into tab\n" + format_exc())
|
||||
pass
|
||||
@@ -181,12 +179,11 @@ def main():
|
||||
if get_effective_user_id() != 0:
|
||||
logger.warning(f"decky is running as an unprivileged user, this is not officially supported and may cause issues")
|
||||
|
||||
# Append the loader's plugin path to the recognized python paths
|
||||
sys.path.append(path.join(path.dirname(__file__), "..", "plugin"))
|
||||
|
||||
# Append the system and user python paths
|
||||
sys.path.extend(get_system_pythonpaths())
|
||||
|
||||
logger.info(f"Starting Decky version {get_loader_version()}")
|
||||
|
||||
loop = new_event_loop()
|
||||
set_event_loop(loop)
|
||||
PluginManager(loop).run()
|
||||
@@ -12,13 +12,15 @@ Some basic migration helpers are available: `migrate_any`, `migrate_settings`, `
|
||||
A logging facility `logger` is available which writes to the recommended location.
|
||||
"""
|
||||
|
||||
__version__ = '0.1.0'
|
||||
__version__ = '1.0.0'
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import logging
|
||||
import time
|
||||
|
||||
from typing import Any
|
||||
|
||||
"""
|
||||
Constants
|
||||
"""
|
||||
@@ -204,6 +206,20 @@ logging.basicConfig(filename=DECKY_PLUGIN_LOG,
|
||||
format='[%(asctime)s][%(levelname)s]: %(message)s',
|
||||
force=True)
|
||||
logger: logging.Logger = logging.getLogger()
|
||||
# Also log to stdout
|
||||
logger.addHandler(logging.StreamHandler())
|
||||
"""The main plugin logger writing to `DECKY_PLUGIN_LOG`."""
|
||||
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
"""
|
||||
Event handling
|
||||
"""
|
||||
# This is overriden with an actual implementation before being passed to any plugins
|
||||
# in ../sandboxed_plugin.py 's initialize function
|
||||
async def emit(event: str, *args: Any) -> None:
|
||||
"""
|
||||
Triggers all event listeners in the frontend waiting for `event`, passing the remaining `*args` as the arguments to each listener function.
|
||||
(Event listeners are set up in the frontend via the `addEventListener` function from `@decky/api`)
|
||||
"""
|
||||
pass
|
||||
@@ -12,10 +12,12 @@ Some basic migration helpers are available: `migrate_any`, `migrate_settings`, `
|
||||
A logging facility `logger` is available which writes to the recommended location.
|
||||
"""
|
||||
|
||||
__version__ = '0.1.0'
|
||||
__version__ = '1.0.0'
|
||||
|
||||
import logging
|
||||
|
||||
from typing import Any
|
||||
|
||||
"""
|
||||
Constants
|
||||
"""
|
||||
@@ -171,3 +173,13 @@ Logging
|
||||
|
||||
logger: logging.Logger
|
||||
"""The main plugin logger writing to `DECKY_PLUGIN_LOG`."""
|
||||
|
||||
"""
|
||||
Event handling
|
||||
"""
|
||||
|
||||
async def emit(event: str, *args: Any) -> None:
|
||||
"""
|
||||
Triggers all event listeners in the frontend waiting for `event`, passing the remaining `*args` as the arguments to each listener function.
|
||||
(Event listeners are set up in the frontend via the `addEventListener` function from `@decky/api`)
|
||||
"""
|
||||
@@ -0,0 +1,36 @@
|
||||
from typing import Any, TypedDict
|
||||
from enum import IntEnum
|
||||
from uuid import uuid4
|
||||
from asyncio import Event
|
||||
|
||||
class SocketMessageType(IntEnum):
|
||||
CALL = 0
|
||||
RESPONSE = 1
|
||||
EVENT = 2
|
||||
|
||||
class SocketResponseDict(TypedDict):
|
||||
type: SocketMessageType
|
||||
id: str
|
||||
success: bool
|
||||
res: Any
|
||||
|
||||
class MethodCallResponse:
|
||||
def __init__(self, success: bool, result: Any) -> None:
|
||||
self.success = success
|
||||
self.result = result
|
||||
|
||||
class MethodCallRequest:
|
||||
def __init__(self) -> None:
|
||||
self.id = str(uuid4())
|
||||
self.event = Event()
|
||||
self.response: MethodCallResponse
|
||||
|
||||
def set_result(self, dc: SocketResponseDict):
|
||||
self.response = MethodCallResponse(dc["success"], dc["res"])
|
||||
self.event.set()
|
||||
|
||||
async def wait_for_result(self):
|
||||
await self.event.wait()
|
||||
if not self.response.success:
|
||||
raise Exception(self.response.result)
|
||||
return self.response.result
|
||||
@@ -0,0 +1,116 @@
|
||||
from asyncio import Task, create_task
|
||||
from json import dumps, load, loads
|
||||
from logging import getLogger
|
||||
from os import path
|
||||
from multiprocessing import Process
|
||||
|
||||
from .sandboxed_plugin import SandboxedPlugin
|
||||
from .messages import MethodCallRequest, SocketMessageType
|
||||
from ..enums import PluginLoadType
|
||||
from ..localplatform.localsocket import LocalSocket
|
||||
from ..helpers import get_homebrew_path, mkdir_as_user
|
||||
|
||||
from typing import Any, Callable, Coroutine, Dict, List
|
||||
|
||||
EmittedEventCallbackType = Callable[[str, Any], Coroutine[Any, Any, Any]]
|
||||
|
||||
class PluginWrapper:
|
||||
def __init__(self, file: str, plugin_directory: str, plugin_path: str, emit_callback: EmittedEventCallbackType) -> None:
|
||||
self.file = file
|
||||
self.plugin_path = plugin_path
|
||||
self.plugin_directory = plugin_directory
|
||||
|
||||
self.version = None
|
||||
|
||||
self.load_type = PluginLoadType.LEGACY_EVAL_IIFE.value
|
||||
|
||||
json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r", encoding="utf-8"))
|
||||
if path.isfile(path.join(plugin_path, plugin_directory, "package.json")):
|
||||
package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r", encoding="utf-8"))
|
||||
self.version = package_json["version"]
|
||||
if ("type" in package_json and package_json["type"] == "module"):
|
||||
self.load_type = PluginLoadType.ESMODULE_V1.value
|
||||
|
||||
self.name = json["name"]
|
||||
self.author = json["author"]
|
||||
self.flags = json["flags"]
|
||||
self.api_version = json["api_version"] if "api_version" in json else 0
|
||||
|
||||
self.passive = not path.isfile(self.file)
|
||||
|
||||
self.log = getLogger("plugin")
|
||||
|
||||
self.sandboxed_plugin = SandboxedPlugin(self.name, self.passive, self.flags, self.file, self.plugin_directory, self.plugin_path, self.version, self.author, self.api_version)
|
||||
# TODO: Maybe make LocalSocket not require on_new_message to make this cleaner
|
||||
self._socket = LocalSocket(self.sandboxed_plugin.on_new_message)
|
||||
self._listener_task: Task[Any]
|
||||
self._method_call_requests: Dict[str, MethodCallRequest] = {}
|
||||
|
||||
self.emitted_event_callback: EmittedEventCallbackType = emit_callback
|
||||
|
||||
# TODO enable this after websocket release
|
||||
self.legacy_method_warning = False
|
||||
|
||||
home = get_homebrew_path()
|
||||
mkdir_as_user(path.join(home, "settings", self.plugin_directory))
|
||||
# TODO maybe dont chown this?
|
||||
mkdir_as_user(path.join(home, "data"))
|
||||
mkdir_as_user(path.join(home, "data", self.plugin_directory))
|
||||
# TODO maybe dont chown this?
|
||||
mkdir_as_user(path.join(home, "logs"))
|
||||
mkdir_as_user(path.join(home, "logs", self.plugin_directory))
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
async def _response_listener(self):
|
||||
while True:
|
||||
try:
|
||||
line = await self._socket.read_single_line()
|
||||
if line != None:
|
||||
res = loads(line)
|
||||
if res["type"] == SocketMessageType.EVENT.value:
|
||||
create_task(self.emitted_event_callback(res["event"], res["args"]))
|
||||
elif res["type"] == SocketMessageType.RESPONSE.value:
|
||||
self._method_call_requests.pop(res["id"]).set_result(res)
|
||||
except:
|
||||
pass
|
||||
|
||||
async def execute_legacy_method(self, method_name: str, kwargs: Dict[Any, Any]):
|
||||
if not self.legacy_method_warning:
|
||||
self.legacy_method_warning = True
|
||||
self.log.warn(f"Plugin {self.name} is using legacy method calls. This will be removed in a future release.")
|
||||
if self.passive:
|
||||
raise RuntimeError("This plugin is passive (aka does not implement main.py)")
|
||||
|
||||
request = MethodCallRequest()
|
||||
await self._socket.get_socket_connection()
|
||||
await self._socket.write_single_line(dumps({ "type": SocketMessageType.CALL, "method": method_name, "args": kwargs, "id": request.id, "legacy": True }, ensure_ascii=False))
|
||||
self._method_call_requests[request.id] = request
|
||||
|
||||
return await request.wait_for_result()
|
||||
|
||||
async def execute_method(self, method_name: str, *args: List[Any]):
|
||||
if self.passive:
|
||||
raise RuntimeError("This plugin is passive (aka does not implement main.py)")
|
||||
|
||||
request = MethodCallRequest()
|
||||
await self._socket.get_socket_connection()
|
||||
await self._socket.write_single_line(dumps({ "type": SocketMessageType.CALL, "method": method_name, "args": args, "id": request.id }, ensure_ascii=False))
|
||||
self._method_call_requests[request.id] = request
|
||||
|
||||
return await request.wait_for_result()
|
||||
|
||||
def start(self):
|
||||
if self.passive:
|
||||
return self
|
||||
Process(target=self.sandboxed_plugin.initialize, args=[self._socket]).start()
|
||||
self._listener_task = create_task(self._response_listener())
|
||||
return self
|
||||
|
||||
async def stop(self, uninstall: bool = False):
|
||||
if hasattr(self, "_socket"):
|
||||
await self._socket.write_single_line(dumps({ "stop": True, "uninstall": uninstall }, ensure_ascii=False))
|
||||
await self._socket.close_socket_connection()
|
||||
if hasattr(self, "_listener_task"):
|
||||
self._listener_task.cancel()
|
||||
@@ -1,51 +1,49 @@
|
||||
import multiprocessing
|
||||
from asyncio import (Lock, get_event_loop, new_event_loop,
|
||||
set_event_loop, sleep)
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from json import dumps, load, loads
|
||||
from logging import getLogger
|
||||
from traceback import format_exc
|
||||
from os import path, environ
|
||||
from signal import SIGINT, signal
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from json import dumps, loads
|
||||
from logging import getLogger
|
||||
from sys import exit, path as syspath, modules as sysmodules
|
||||
from typing import Any, Dict
|
||||
from .localsocket import LocalSocket
|
||||
from .localplatform import setgid, setuid, get_username, get_home_path
|
||||
from .customtypes import UserType
|
||||
from . import helpers
|
||||
from traceback import format_exc
|
||||
from asyncio import (get_event_loop, new_event_loop,
|
||||
set_event_loop, sleep)
|
||||
|
||||
class PluginWrapper:
|
||||
def __init__(self, file: str, plugin_directory: str, plugin_path: str) -> None:
|
||||
from .messages import SocketResponseDict, SocketMessageType
|
||||
from ..localplatform.localsocket import LocalSocket
|
||||
from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path
|
||||
from ..enums import UserType
|
||||
from .. import helpers
|
||||
|
||||
from typing import List, TypeVar, Any
|
||||
|
||||
DataType = TypeVar("DataType")
|
||||
|
||||
class SandboxedPlugin:
|
||||
def __init__(self,
|
||||
name: str,
|
||||
passive: bool,
|
||||
flags: List[str],
|
||||
file: str,
|
||||
plugin_directory: str,
|
||||
plugin_path: str,
|
||||
version: str|None,
|
||||
author: str,
|
||||
api_version: int) -> None:
|
||||
self.name = name
|
||||
self.passive = passive
|
||||
self.flags = flags
|
||||
self.file = file
|
||||
self.plugin_path = plugin_path
|
||||
self.plugin_directory = plugin_directory
|
||||
self.method_call_lock = Lock()
|
||||
self.socket: LocalSocket = LocalSocket(self._on_new_message)
|
||||
|
||||
self.version = None
|
||||
|
||||
json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r", encoding="utf-8"))
|
||||
if path.isfile(path.join(plugin_path, plugin_directory, "package.json")):
|
||||
package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r", encoding="utf-8"))
|
||||
self.version = package_json["version"]
|
||||
|
||||
self.legacy = False
|
||||
self.main_view_html = json["main_view_html"] if "main_view_html" in json else ""
|
||||
self.tile_view_html = json["tile_view_html"] if "tile_view_html" in json else ""
|
||||
self.legacy = self.main_view_html or self.tile_view_html
|
||||
|
||||
self.name = json["name"]
|
||||
self.author = json["author"]
|
||||
self.flags = json["flags"]
|
||||
self.version = version
|
||||
self.author = author
|
||||
self.api_version = api_version
|
||||
|
||||
self.log = getLogger("plugin")
|
||||
|
||||
self.passive = not path.isfile(self.file)
|
||||
def initialize(self, socket: LocalSocket):
|
||||
self._socket = socket
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def _init(self):
|
||||
try:
|
||||
signal(SIGINT, lambda s, f: exit(0))
|
||||
|
||||
@@ -62,14 +60,8 @@ class PluginWrapper:
|
||||
environ["DECKY_USER_HOME"] = helpers.get_home_path()
|
||||
environ["DECKY_HOME"] = helpers.get_homebrew_path()
|
||||
environ["DECKY_PLUGIN_SETTINGS_DIR"] = path.join(environ["DECKY_HOME"], "settings", self.plugin_directory)
|
||||
helpers.mkdir_as_user(path.join(environ["DECKY_HOME"], "settings"))
|
||||
helpers.mkdir_as_user(environ["DECKY_PLUGIN_SETTINGS_DIR"])
|
||||
environ["DECKY_PLUGIN_RUNTIME_DIR"] = path.join(environ["DECKY_HOME"], "data", self.plugin_directory)
|
||||
helpers.mkdir_as_user(path.join(environ["DECKY_HOME"], "data"))
|
||||
helpers.mkdir_as_user(environ["DECKY_PLUGIN_RUNTIME_DIR"])
|
||||
environ["DECKY_PLUGIN_LOG_DIR"] = path.join(environ["DECKY_HOME"], "logs", self.plugin_directory)
|
||||
helpers.mkdir_as_user(path.join(environ["DECKY_HOME"], "logs"))
|
||||
helpers.mkdir_as_user(environ["DECKY_PLUGIN_LOG_DIR"])
|
||||
environ["DECKY_PLUGIN_DIR"] = path.join(self.plugin_path, self.plugin_directory)
|
||||
environ["DECKY_PLUGIN_NAME"] = self.name
|
||||
if self.version:
|
||||
@@ -80,22 +72,46 @@ class PluginWrapper:
|
||||
syspath.append(path.join(environ["DECKY_PLUGIN_DIR"], "py_modules"))
|
||||
|
||||
#TODO: FIX IN A LESS CURSED WAY
|
||||
keys = [key.replace("src.", "") for key in sysmodules if key.startswith("src.")]
|
||||
keys = [key for key in sysmodules if key.startswith("decky_loader.")]
|
||||
for key in keys:
|
||||
sysmodules[key] = sysmodules["src"].__dict__[key]
|
||||
sysmodules[key.replace("decky_loader.", "")] = sysmodules[key]
|
||||
|
||||
from .imports import decky
|
||||
async def emit(event: str, *args: Any) -> None:
|
||||
await self._socket.write_single_line_server(dumps({
|
||||
"type": SocketMessageType.EVENT,
|
||||
"event": event,
|
||||
"args": args
|
||||
}))
|
||||
# copy the docstring over so we don't have to duplicate it
|
||||
emit.__doc__ = decky.emit.__doc__
|
||||
decky.emit = emit
|
||||
sysmodules["decky"] = decky
|
||||
# provided for compatibility
|
||||
sysmodules["decky_plugin"] = decky
|
||||
|
||||
spec = spec_from_file_location("_", self.file)
|
||||
assert spec is not None
|
||||
module = module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
self.Plugin = module.Plugin
|
||||
# TODO fix self weirdness once plugin.json versioning is done. need this before WS release!
|
||||
if self.api_version > 0:
|
||||
self.Plugin = module.Plugin()
|
||||
else:
|
||||
self.Plugin = module.Plugin
|
||||
|
||||
if hasattr(self.Plugin, "_migration"):
|
||||
get_event_loop().run_until_complete(self.Plugin._migration(self.Plugin))
|
||||
if self.api_version > 0:
|
||||
get_event_loop().run_until_complete(self.Plugin._migration())
|
||||
else:
|
||||
get_event_loop().run_until_complete(self.Plugin._migration(self.Plugin))
|
||||
if hasattr(self.Plugin, "_main"):
|
||||
get_event_loop().create_task(self.Plugin._main(self.Plugin))
|
||||
get_event_loop().create_task(self.socket.setup_server())
|
||||
if self.api_version > 0:
|
||||
get_event_loop().create_task(self.Plugin._main())
|
||||
else:
|
||||
get_event_loop().create_task(self.Plugin._main(self.Plugin))
|
||||
get_event_loop().create_task(socket.setup_server())
|
||||
get_event_loop().run_forever()
|
||||
except:
|
||||
self.log.error("Failed to start " + self.name + "!\n" + format_exc())
|
||||
@@ -105,7 +121,10 @@ class PluginWrapper:
|
||||
try:
|
||||
self.log.info("Attempting to unload with plugin " + self.name + "'s \"_unload\" function.\n")
|
||||
if hasattr(self.Plugin, "_unload"):
|
||||
await self.Plugin._unload(self.Plugin)
|
||||
if self.api_version > 0:
|
||||
await self.Plugin._unload()
|
||||
else:
|
||||
await self.Plugin._unload(self.Plugin)
|
||||
self.log.info("Unloaded " + self.name + "\n")
|
||||
else:
|
||||
self.log.info("Could not find \"_unload\" in " + self.name + "'s main.py" + "\n")
|
||||
@@ -117,7 +136,10 @@ class PluginWrapper:
|
||||
try:
|
||||
self.log.info("Attempting to uninstall with plugin " + self.name + "'s \"_uninstall\" function.\n")
|
||||
if hasattr(self.Plugin, "_uninstall"):
|
||||
await self.Plugin._uninstall(self.Plugin)
|
||||
if self.api_version > 0:
|
||||
await self.Plugin._uninstall()
|
||||
else:
|
||||
await self.Plugin._uninstall(self.Plugin)
|
||||
self.log.info("Uninstalled " + self.name + "\n")
|
||||
else:
|
||||
self.log.info("Could not find \"_uninstall\" in " + self.name + "'s main.py" + "\n")
|
||||
@@ -125,7 +147,7 @@ class PluginWrapper:
|
||||
self.log.error("Failed to uninstall " + self.name + "!\n" + format_exc())
|
||||
exit(0)
|
||||
|
||||
async def _on_new_message(self, message : str) -> str|None:
|
||||
async def on_new_message(self, message : str) -> str|None:
|
||||
data = loads(message)
|
||||
|
||||
if "stop" in data:
|
||||
@@ -142,44 +164,20 @@ class PluginWrapper:
|
||||
get_event_loop().close()
|
||||
raise Exception("Closing message listener")
|
||||
|
||||
# TODO there is definitely a better way to type this
|
||||
d: Dict[str, Any] = {"res": None, "success": True}
|
||||
d: SocketResponseDict = {"type": SocketMessageType.RESPONSE, "res": None, "success": True, "id": data["id"]}
|
||||
try:
|
||||
d["res"] = await getattr(self.Plugin, data["method"])(self.Plugin, **data["args"])
|
||||
if data.get("legacy"):
|
||||
if self.api_version > 0:
|
||||
raise Exception("Legacy methods may not be used on api_version > 0")
|
||||
# Legacy kwargs
|
||||
d["res"] = await getattr(self.Plugin, data["method"])(self.Plugin, **data["args"])
|
||||
else:
|
||||
if self.api_version < 1 :
|
||||
raise Exception("api_version 1 or newer is required to call methods with index-based arguments")
|
||||
# New args
|
||||
d["res"] = await getattr(self.Plugin, data["method"])(*data["args"])
|
||||
except Exception as e:
|
||||
d["res"] = str(e)
|
||||
d["success"] = False
|
||||
finally:
|
||||
return dumps(d, ensure_ascii=False)
|
||||
|
||||
def start(self):
|
||||
if self.passive:
|
||||
return self
|
||||
multiprocessing.Process(target=self._init).start()
|
||||
return self
|
||||
|
||||
def stop(self, uninstall: bool = False):
|
||||
if self.passive:
|
||||
return
|
||||
|
||||
async def _(self: PluginWrapper):
|
||||
await self.socket.write_single_line(dumps({ "stop": True, "uninstall": uninstall }, ensure_ascii=False))
|
||||
await self.socket.close_socket_connection()
|
||||
|
||||
get_event_loop().create_task(_(self))
|
||||
|
||||
async def execute_method(self, method_name: str, kwargs: Dict[Any, Any]):
|
||||
if self.passive:
|
||||
raise RuntimeError("This plugin is passive (aka does not implement main.py)")
|
||||
async with self.method_call_lock:
|
||||
# reader, writer =
|
||||
await self.socket.get_socket_connection()
|
||||
|
||||
await self.socket.write_single_line(dumps({ "method": method_name, "args": kwargs }, ensure_ascii=False))
|
||||
|
||||
line = await self.socket.read_single_line()
|
||||
if line != None:
|
||||
res = loads(line)
|
||||
if not res["success"]:
|
||||
raise Exception(res["res"])
|
||||
return res["res"]
|
||||
@@ -1,8 +1,8 @@
|
||||
from json import dump, load
|
||||
from os import mkdir, path, listdir, rename
|
||||
from typing import Any, Dict
|
||||
from .localplatform import chown, folder_owner, get_chown_plugin_path
|
||||
from .customtypes import UserType
|
||||
from .localplatform.localplatform import chown, folder_owner, get_chown_plugin_path
|
||||
from .enums import UserType
|
||||
|
||||
from .helpers import get_homebrew_path
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
from __future__ import annotations
|
||||
from asyncio import sleep
|
||||
from json.decoder import JSONDecodeError
|
||||
from logging import getLogger
|
||||
import os
|
||||
from os import getcwd, path, remove
|
||||
from typing import TYPE_CHECKING, List, TypedDict
|
||||
if TYPE_CHECKING:
|
||||
from .main import PluginManager
|
||||
from .localplatform.localplatform import chmod, service_restart, service_stop, ON_LINUX, ON_WINDOWS, get_keep_systemd_service, get_selinux
|
||||
import shutil
|
||||
from typing import List, TYPE_CHECKING, TypedDict
|
||||
import zipfile
|
||||
|
||||
from aiohttp import ClientSession, web
|
||||
from aiohttp import ClientSession
|
||||
|
||||
from . import helpers
|
||||
from .injector import get_gamepadui_tab
|
||||
from .localplatform import (
|
||||
ON_LINUX,
|
||||
ON_WINDOWS,
|
||||
chmod,
|
||||
get_keep_systemd_service,
|
||||
get_selinux,
|
||||
service_restart,
|
||||
)
|
||||
from .settings import SettingsManager
|
||||
if TYPE_CHECKING:
|
||||
from .main import PluginManager
|
||||
@@ -40,21 +35,10 @@ class TestingVersion(TypedDict):
|
||||
link: str
|
||||
head_sha: str
|
||||
|
||||
|
||||
class Updater:
|
||||
def __init__(self, context: PluginManager) -> None:
|
||||
self.context = context
|
||||
self.settings = self.context.settings
|
||||
# Exposes updater methods to frontend
|
||||
self.updater_methods = {
|
||||
"get_branch": self._get_branch,
|
||||
"get_version": self.get_version,
|
||||
"do_update": self.do_update,
|
||||
"do_restart": self.do_restart,
|
||||
"check_for_updates": self.check_for_updates,
|
||||
"get_testing_versions": self.get_testing_versions,
|
||||
"download_testing_version": self.download_testing_version
|
||||
}
|
||||
self.remoteVer: RemoteVer | None = None
|
||||
self.allRemoteVers: List[RemoteVer] = []
|
||||
self.localVer = helpers.get_loader_version()
|
||||
@@ -66,27 +50,15 @@ class Updater:
|
||||
logger.error("Current branch could not be determined, defaulting to \"Stable\"")
|
||||
|
||||
if context:
|
||||
context.web_app.add_routes([
|
||||
web.post("/updater/{method_name}", self._handle_server_method_call)
|
||||
])
|
||||
context.ws.add_route("updater/get_version_info", self.get_version_info);
|
||||
context.ws.add_route("updater/check_for_updates", self.check_for_updates);
|
||||
context.ws.add_route("updater/do_restart", self.do_restart);
|
||||
context.ws.add_route("updater/do_shutdown", self.do_shutdown);
|
||||
context.ws.add_route("updater/do_update", self.do_update);
|
||||
context.ws.add_route("updater/get_testing_versions", self.get_testing_versions);
|
||||
context.ws.add_route("updater/download_testing_version", self.download_testing_version);
|
||||
context.loop.create_task(self.version_reloader())
|
||||
|
||||
async def _handle_server_method_call(self, request: web.Request):
|
||||
method_name = request.match_info["method_name"]
|
||||
try:
|
||||
args = await request.json()
|
||||
except JSONDecodeError:
|
||||
args = {}
|
||||
res = {}
|
||||
try:
|
||||
r = await self.updater_methods[method_name](**args) # type: ignore
|
||||
res["result"] = r
|
||||
res["success"] = True
|
||||
except Exception as e:
|
||||
res["result"] = str(e)
|
||||
res["success"] = False
|
||||
return web.json_response(res)
|
||||
|
||||
def get_branch(self, manager: SettingsManager):
|
||||
ver = manager.getSetting("branch", -1)
|
||||
logger.debug("current branch: %i" % ver)
|
||||
@@ -119,7 +91,7 @@ class Updater:
|
||||
url = "https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/dist/plugin_loader-prerelease.service"
|
||||
return str(url)
|
||||
|
||||
async def get_version(self):
|
||||
async def get_version_info(self):
|
||||
return {
|
||||
"current": self.localVer,
|
||||
"remote": self.remoteVer,
|
||||
@@ -131,7 +103,7 @@ class Updater:
|
||||
logger.debug("checking for updates")
|
||||
selectedBranch = self.get_branch(self.context.settings)
|
||||
async with ClientSession() as web:
|
||||
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", ssl=helpers.get_ssl_context()) as res:
|
||||
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", headers={'X-GitHub-Api-Version': '2022-11-28'}, ssl=helpers.get_ssl_context()) as res:
|
||||
remoteVersions: List[RemoteVer] = await res.json()
|
||||
if selectedBranch == 0:
|
||||
logger.debug("release type: release")
|
||||
@@ -154,9 +126,8 @@ class Updater:
|
||||
logger.error("release type: NOT FOUND")
|
||||
raise ValueError("no valid branch found")
|
||||
logger.info("Updated remote version information")
|
||||
tab = await get_gamepadui_tab()
|
||||
await tab.evaluate_js(f"window.DeckyPluginLoader.notifyUpdates()", False, True, False)
|
||||
return await self.get_version()
|
||||
await self.context.ws.emit("loader/notify_updates")
|
||||
return await self.get_version_info()
|
||||
|
||||
async def version_reloader(self):
|
||||
await sleep(30)
|
||||
@@ -167,27 +138,30 @@ class Updater:
|
||||
pass
|
||||
await sleep(60 * 60 * 6) # 6 hours
|
||||
|
||||
async def download_decky_binary(self, download_url: str, version: str, is_zip: bool = False):
|
||||
async def download_decky_binary(self, download_url: str, version: str, is_zip: bool = False, size_in_bytes: int | None = None):
|
||||
download_filename = "PluginLoader" if ON_LINUX else "PluginLoader.exe"
|
||||
download_temp_filename = download_filename + ".new"
|
||||
tab = await get_gamepadui_tab()
|
||||
await tab.open_websocket()
|
||||
|
||||
if size_in_bytes == None:
|
||||
size_in_bytes = 26214400 # 25MiB, a reasonable overestimate (19.6MiB as of 2024/02/25)
|
||||
|
||||
async with ClientSession() as web:
|
||||
logger.debug("Downloading binary")
|
||||
async with web.request("GET", download_url, ssl=helpers.get_ssl_context(), allow_redirects=True) as res:
|
||||
total = int(res.headers.get('content-length', 0))
|
||||
total = int(res.headers.get('content-length', size_in_bytes))
|
||||
if total == 0: total = 1
|
||||
with open(path.join(getcwd(), download_temp_filename), "wb") as out:
|
||||
progress = 0
|
||||
raw = 0
|
||||
async for c in res.content.iter_chunked(512):
|
||||
out.write(c)
|
||||
if total != 0:
|
||||
raw += len(c)
|
||||
new_progress = round((raw / total) * 100)
|
||||
if progress != new_progress:
|
||||
self.context.loop.create_task(tab.evaluate_js(f"window.DeckyUpdater.updateProgress({new_progress})", False, False, False))
|
||||
progress = new_progress
|
||||
raw += len(c)
|
||||
new_progress = round((raw / total) * 100)
|
||||
if progress != new_progress:
|
||||
self.context.loop.create_task(self.context.ws.emit("updater/update_download_percentage", new_progress))
|
||||
progress = new_progress
|
||||
|
||||
with open(path.join(getcwd(), ".loader.version"), "w", encoding="utf-8") as out:
|
||||
out.write(version)
|
||||
@@ -210,9 +184,9 @@ class Updater:
|
||||
logger.info(f"Setting the executable flag with chcon returned {await process.wait()}")
|
||||
|
||||
logger.info("Updated loader installation.")
|
||||
await tab.evaluate_js("window.DeckyUpdater.finish()", False, False)
|
||||
await self.do_restart()
|
||||
await self.context.ws.emit("updater/finish_download")
|
||||
await tab.close_websocket()
|
||||
await self.do_restart()
|
||||
|
||||
async def do_update(self):
|
||||
logger.debug("Starting update.")
|
||||
@@ -269,6 +243,9 @@ class Updater:
|
||||
async def do_restart(self):
|
||||
await service_restart("plugin_loader")
|
||||
|
||||
async def do_shutdown(self):
|
||||
await service_stop("plugin_loader")
|
||||
|
||||
async def get_testing_versions(self) -> List[TestingVersion]:
|
||||
result: List[TestingVersion] = []
|
||||
async with ClientSession() as web:
|
||||
@@ -289,7 +266,7 @@ class Updater:
|
||||
#Get all the associated workflow run for the given sha_id code hash
|
||||
async with ClientSession() as web:
|
||||
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/actions/runs",
|
||||
headers={'X-GitHub-Api-Version': '2022-11-28'}, params={'event':'pull_request', 'head_sha': sha_id}, ssl=helpers.get_ssl_context()) as res:
|
||||
headers={'X-GitHub-Api-Version': '2022-11-28'}, params={'head_sha': sha_id}, ssl=helpers.get_ssl_context()) as res:
|
||||
works = await res.json()
|
||||
#Iterate over the workflow_run to get the two builds if they exists
|
||||
for work in works['workflow_runs']:
|
||||
@@ -307,6 +284,10 @@ class Updater:
|
||||
#If the request found at least one artifact to download...
|
||||
if int(jresp['total_count']) != 0:
|
||||
# this assumes that the artifact we want is the first one!
|
||||
down_link = f"https://nightly.link/SteamDeckHomebrew/decky-loader/actions/artifacts/{jresp['artifacts'][0]['id']}.zip"
|
||||
artifact = jresp['artifacts'][0]
|
||||
down_link = f"https://nightly.link/SteamDeckHomebrew/decky-loader/actions/artifacts/{artifact['id']}.zip"
|
||||
#Then fetch it and restart itself
|
||||
await self.download_decky_binary(down_link, f'PR-{pr_id}' , True)
|
||||
await self.download_decky_binary(down_link, f'PR-{pr_id}', is_zip=True, size_in_bytes=artifact.get('size_in_bytes',None))
|
||||
else:
|
||||
logger.error("workflow run not found", str(works))
|
||||
raise Exception("Workflow run not found.")
|
||||
@@ -1,14 +1,16 @@
|
||||
from __future__ import annotations
|
||||
from os import stat_result
|
||||
import uuid
|
||||
from urllib.parse import unquote
|
||||
from json.decoder import JSONDecodeError
|
||||
from os.path import splitext
|
||||
import re
|
||||
from traceback import format_exc
|
||||
from stat import FILE_ATTRIBUTE_HIDDEN # type: ignore
|
||||
from stat import FILE_ATTRIBUTE_HIDDEN # pyright: ignore [reportAttributeAccessIssue, reportUnknownVariableType]
|
||||
|
||||
from asyncio import StreamReader, StreamWriter, start_server, gather, open_connection
|
||||
from aiohttp import ClientSession, web
|
||||
from aiohttp import ClientSession
|
||||
from aiohttp.web import Request, StreamResponse, Response, json_response, post
|
||||
from typing import TYPE_CHECKING, Callable, Coroutine, Dict, Any, List, TypedDict
|
||||
|
||||
from logging import getLogger
|
||||
@@ -18,36 +20,32 @@ from .browser import PluginInstallRequest, PluginInstallType
|
||||
if TYPE_CHECKING:
|
||||
from .main import PluginManager
|
||||
from .injector import inject_to_tab, get_gamepadui_tab, close_old_tabs, get_tab
|
||||
from .localplatform import ON_WINDOWS
|
||||
from .localplatform.localplatform import ON_WINDOWS
|
||||
from . import helpers
|
||||
from .localplatform import service_stop, service_start, get_home_path, get_username
|
||||
from .localplatform.localplatform import service_stop, service_start, get_home_path, get_username
|
||||
|
||||
class FilePickerObj(TypedDict):
|
||||
file: Path
|
||||
filest: stat_result
|
||||
is_dir: bool
|
||||
|
||||
decky_header_regex = re.compile("X-Decky-(.*)")
|
||||
extra_header_regex = re.compile("X-Decky-Header-(.*)")
|
||||
|
||||
excluded_default_headers = ["Host", "Origin", "Sec-Fetch-Site", "Sec-Fetch-Mode", "Sec-Fetch-Dest"]
|
||||
|
||||
class Utilities:
|
||||
def __init__(self, context: PluginManager) -> None:
|
||||
self.context = context
|
||||
self.util_methods: Dict[str, Callable[..., Coroutine[Any, Any, Any]]] = {
|
||||
self.legacy_util_methods: Dict[str, Callable[..., Coroutine[Any, Any, Any]]] = {
|
||||
"ping": self.ping,
|
||||
"http_request": self.http_request,
|
||||
"install_plugin": self.install_plugin,
|
||||
"install_plugins": self.install_plugins,
|
||||
"cancel_plugin_install": self.cancel_plugin_install,
|
||||
"confirm_plugin_install": self.confirm_plugin_install,
|
||||
"uninstall_plugin": self.uninstall_plugin,
|
||||
"http_request": self.http_request_legacy,
|
||||
"execute_in_tab": self.execute_in_tab,
|
||||
"inject_css_into_tab": self.inject_css_into_tab,
|
||||
"remove_css_from_tab": self.remove_css_from_tab,
|
||||
"allow_remote_debugging": self.allow_remote_debugging,
|
||||
"disallow_remote_debugging": self.disallow_remote_debugging,
|
||||
"set_setting": self.set_setting,
|
||||
"get_setting": self.get_setting,
|
||||
"filepicker_ls": self.filepicker_ls,
|
||||
"disable_rdt": self.disable_rdt,
|
||||
"enable_rdt": self.enable_rdt,
|
||||
"get_tab_id": self.get_tab_id,
|
||||
"get_user_info": self.get_user_info,
|
||||
}
|
||||
@@ -59,11 +57,38 @@ class Utilities:
|
||||
self.rdt_proxy_task = None
|
||||
|
||||
if context:
|
||||
context.ws.add_route("utilities/ping", self.ping)
|
||||
context.ws.add_route("utilities/settings/get", self.get_setting)
|
||||
context.ws.add_route("utilities/settings/set", self.set_setting)
|
||||
context.ws.add_route("utilities/install_plugin", self.install_plugin)
|
||||
context.ws.add_route("utilities/install_plugins", self.install_plugins)
|
||||
context.ws.add_route("utilities/cancel_plugin_install", self.cancel_plugin_install)
|
||||
context.ws.add_route("utilities/confirm_plugin_install", self.confirm_plugin_install)
|
||||
context.ws.add_route("utilities/uninstall_plugin", self.uninstall_plugin)
|
||||
context.ws.add_route("utilities/execute_in_tab", self.execute_in_tab)
|
||||
context.ws.add_route("utilities/inject_css_into_tab", self.inject_css_into_tab)
|
||||
context.ws.add_route("utilities/remove_css_from_tab", self.remove_css_from_tab)
|
||||
context.ws.add_route("utilities/allow_remote_debugging", self.allow_remote_debugging)
|
||||
context.ws.add_route("utilities/disallow_remote_debugging", self.disallow_remote_debugging)
|
||||
context.ws.add_route("utilities/start_ssh", self.allow_remote_debugging)
|
||||
context.ws.add_route("utilities/stop_ssh", self.allow_remote_debugging)
|
||||
context.ws.add_route("utilities/filepicker_ls", self.filepicker_ls)
|
||||
context.ws.add_route("utilities/disable_rdt", self.disable_rdt)
|
||||
context.ws.add_route("utilities/enable_rdt", self.enable_rdt)
|
||||
context.ws.add_route("utilities/get_tab_id", self.get_tab_id)
|
||||
context.ws.add_route("utilities/get_user_info", self.get_user_info)
|
||||
context.ws.add_route("utilities/http_request", self.http_request_legacy)
|
||||
context.ws.add_route("utilities/_call_legacy_utility", self._call_legacy_utility)
|
||||
|
||||
context.web_app.add_routes([
|
||||
web.post("/methods/{method_name}", self._handle_server_method_call)
|
||||
post("/methods/{method_name}", self._handle_legacy_server_method_call)
|
||||
])
|
||||
|
||||
async def _handle_server_method_call(self, request: web.Request):
|
||||
for method in ('GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'):
|
||||
context.web_app.router.add_route(method, "/fetch", self.http_request)
|
||||
|
||||
|
||||
async def _handle_legacy_server_method_call(self, request: Request) -> Response:
|
||||
method_name = request.match_info["method_name"]
|
||||
try:
|
||||
args = await request.json()
|
||||
@@ -71,13 +96,25 @@ class Utilities:
|
||||
args = {}
|
||||
res = {}
|
||||
try:
|
||||
r = await self.util_methods[method_name](**args)
|
||||
r = await self.legacy_util_methods[method_name](**args)
|
||||
res["result"] = r
|
||||
res["success"] = True
|
||||
except Exception as e:
|
||||
res["result"] = str(e)
|
||||
res["success"] = False
|
||||
return web.json_response(res)
|
||||
return json_response(res)
|
||||
|
||||
async def _call_legacy_utility(self, method_name: str, kwargs: Dict[Any, Any]) -> Any:
|
||||
self.logger.debug(f"Calling utility {method_name} with legacy kwargs");
|
||||
res: Dict[Any, Any] = {}
|
||||
try:
|
||||
r = await self.legacy_util_methods[method_name](**kwargs)
|
||||
res["result"] = r
|
||||
res["success"] = True
|
||||
except Exception as e:
|
||||
res["result"] = str(e)
|
||||
res["success"] = False
|
||||
return res
|
||||
|
||||
async def install_plugin(self, artifact: str="", name: str="No name", version: str="dev", hash: str="", install_type: PluginInstallType=PluginInstallType.INSTALL):
|
||||
return await self.context.plugin_browser.request_plugin_install(
|
||||
@@ -102,9 +139,65 @@ class Utilities:
|
||||
async def uninstall_plugin(self, name: str):
|
||||
return await self.context.plugin_browser.uninstall_plugin(name)
|
||||
|
||||
async def http_request(self, method: str="", url: str="", **kwargs: Any):
|
||||
# Loosely based on https://gist.github.com/mosquito/4dbfacd51e751827cda7ec9761273e95#file-proxy-py
|
||||
async def http_request(self, req: Request) -> StreamResponse:
|
||||
if req.headers.get('X-Decky-Auth', '') != helpers.get_csrf_token() and req.query.get('auth', '') != helpers.get_csrf_token():
|
||||
return Response(text='Forbidden', status=403)
|
||||
|
||||
url = req.headers["X-Decky-Fetch-URL"] if "X-Decky-Fetch-URL" in req.headers else unquote(req.query.get('fetch_url', ''))
|
||||
self.logger.info(f"Preparing {req.method} request to {url}")
|
||||
|
||||
headers = dict(req.headers)
|
||||
|
||||
headers["User-Agent"] = helpers.user_agent
|
||||
|
||||
for excluded_header in excluded_default_headers:
|
||||
self.logger.debug(f"Excluding default header {excluded_header}")
|
||||
if excluded_header in headers:
|
||||
del headers[excluded_header]
|
||||
|
||||
if "X-Decky-Fetch-Excluded-Headers" in req.headers:
|
||||
for excluded_header in req.headers["X-Decky-Fetch-Excluded-Headers"].split(", "):
|
||||
self.logger.debug(f"Excluding header {excluded_header}")
|
||||
if excluded_header in headers:
|
||||
del headers[excluded_header]
|
||||
|
||||
for header in req.headers:
|
||||
match = extra_header_regex.search(header)
|
||||
if match:
|
||||
header_name = match.group(1)
|
||||
header_value = req.headers[header]
|
||||
self.logger.debug(f"Adding extra header {header_name}: {header_value}")
|
||||
headers[header_name] = header_value
|
||||
|
||||
for header in list(headers.keys()):
|
||||
match = decky_header_regex.search(header)
|
||||
if match:
|
||||
self.logger.debug(f"Removing decky header {header} from request")
|
||||
del headers[header]
|
||||
|
||||
self.logger.debug(f"Final request headers: {headers}")
|
||||
|
||||
body = await req.read() # TODO can this also be streamed?
|
||||
|
||||
async with ClientSession() as web:
|
||||
res = await web.request(method, url, ssl=helpers.get_ssl_context(), **kwargs)
|
||||
async with web.request(req.method, url, headers=headers, data=body, ssl=helpers.get_ssl_context()) as web_res:
|
||||
res = StreamResponse(headers=web_res.headers, status=web_res.status)
|
||||
if web_res.headers.get('Transfer-Encoding', '').lower() == 'chunked':
|
||||
res.enable_chunked_encoding()
|
||||
|
||||
await res.prepare(req)
|
||||
self.logger.debug(f"Starting stream for {url}")
|
||||
async for data in web_res.content.iter_any():
|
||||
await res.write(data)
|
||||
if data:
|
||||
await res.drain()
|
||||
self.logger.debug(f"Finished stream for {url}")
|
||||
return res
|
||||
|
||||
async def http_request_legacy(self, method: str, url: str, extra_opts: Any = {}):
|
||||
async with ClientSession() as web:
|
||||
res = await web.request(method, url, ssl=helpers.get_ssl_context(), **extra_opts)
|
||||
text = await res.text()
|
||||
return {
|
||||
"status": res.status,
|
||||
@@ -135,62 +228,40 @@ class Utilities:
|
||||
"result": e
|
||||
}
|
||||
|
||||
async def inject_css_into_tab(self, tab: str, style: str):
|
||||
try:
|
||||
css_id = str(uuid.uuid4())
|
||||
async def inject_css_into_tab(self, tab: str, style: str) -> str:
|
||||
css_id = str(uuid.uuid4())
|
||||
|
||||
result = await inject_to_tab(tab,
|
||||
f"""
|
||||
(function() {{
|
||||
const style = document.createElement('style');
|
||||
style.id = "{css_id}";
|
||||
document.head.append(style);
|
||||
style.textContent = `{style}`;
|
||||
}})()
|
||||
""", False)
|
||||
result = await inject_to_tab(tab,
|
||||
f"""
|
||||
(function() {{
|
||||
const style = document.createElement('style');
|
||||
style.id = "{css_id}";
|
||||
document.head.append(style);
|
||||
style.textContent = `{style}`;
|
||||
}})()
|
||||
""", False)
|
||||
assert result is not None # TODO remove this once it has proper typings
|
||||
if "exceptionDetails" in result["result"]:
|
||||
raise result["result"]["exceptionDetails"]
|
||||
|
||||
if result and "exceptionDetails" in result["result"]:
|
||||
return {
|
||||
"success": False,
|
||||
"result": result["result"]
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"result": css_id
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"result": e
|
||||
}
|
||||
return css_id
|
||||
|
||||
async def remove_css_from_tab(self, tab: str, css_id: str):
|
||||
try:
|
||||
result = await inject_to_tab(tab,
|
||||
f"""
|
||||
(function() {{
|
||||
let style = document.getElementById("{css_id}");
|
||||
result = await inject_to_tab(tab,
|
||||
f"""
|
||||
(function() {{
|
||||
let style = document.getElementById("{css_id}");
|
||||
|
||||
if (style.nodeName.toLowerCase() == 'style')
|
||||
style.parentNode.removeChild(style);
|
||||
}})()
|
||||
""", False)
|
||||
if (style.nodeName.toLowerCase() == 'style')
|
||||
style.parentNode.removeChild(style);
|
||||
}})()
|
||||
""", False)
|
||||
|
||||
assert result
|
||||
if "exceptionDetails" in result["result"]:
|
||||
raise result["result"]["exceptionDetails"]
|
||||
|
||||
if result and "exceptionDetails" in result["result"]:
|
||||
return {
|
||||
"success": False,
|
||||
"result": result
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"result": e
|
||||
}
|
||||
return
|
||||
|
||||
async def get_setting(self, key: str, default: Any):
|
||||
return self.context.settings.getSetting(key, default)
|
||||
@@ -206,13 +277,21 @@ class Utilities:
|
||||
await service_stop(helpers.REMOTE_DEBUGGER_UNIT)
|
||||
return True
|
||||
|
||||
async def start_ssh(self):
|
||||
await service_start(helpers.SSHD_UNIT)
|
||||
return True
|
||||
|
||||
async def stop_ssh(self):
|
||||
await service_stop(helpers.SSHD_UNIT)
|
||||
return True
|
||||
|
||||
async def filepicker_ls(self,
|
||||
path : str | None = None,
|
||||
path: str | None = None,
|
||||
include_files: bool = True,
|
||||
include_folders: bool = True,
|
||||
include_ext: list[str] = [],
|
||||
include_ext: list[str] | None = None,
|
||||
include_hidden: bool = False,
|
||||
order_by: str = "name_asc",
|
||||
order_by: str = "name_desc",
|
||||
filter_for: str | None = None,
|
||||
page: int = 1,
|
||||
max: int = 1000):
|
||||
@@ -237,7 +316,7 @@ class Utilities:
|
||||
folders.append({"file": file, "filest": filest, "is_dir": True})
|
||||
elif include_files:
|
||||
# Handle requested extensions if present
|
||||
if len(include_ext) == 0 or 'all_files' in include_ext \
|
||||
if include_ext == None or len(include_ext) == 0 or 'all_files' in include_ext \
|
||||
or splitext(file.name)[1].lstrip('.').upper() in (ext.upper() for ext in include_ext):
|
||||
if (is_hidden and include_hidden) or not is_hidden:
|
||||
files.append({"file": file, "filest": filest, "is_dir": False})
|
||||
@@ -0,0 +1,135 @@
|
||||
from logging import getLogger
|
||||
|
||||
from asyncio import AbstractEventLoop, create_task
|
||||
|
||||
from aiohttp import WSMsgType, WSMessage
|
||||
from aiohttp.web import Application, WebSocketResponse, Request, Response, get
|
||||
|
||||
from enum import IntEnum
|
||||
|
||||
from typing import Callable, Coroutine, Dict, Any, cast, TypeVar
|
||||
|
||||
from traceback import format_exc
|
||||
|
||||
from .helpers import get_csrf_token
|
||||
|
||||
class MessageType(IntEnum):
|
||||
ERROR = -1
|
||||
# Call-reply, Frontend -> Backend -> Frontend
|
||||
CALL = 0
|
||||
REPLY = 1
|
||||
# Pub/Sub, Backend -> Frontend
|
||||
EVENT = 3
|
||||
|
||||
# WSMessage with slightly better typings
|
||||
class WSMessageExtra(WSMessage):
|
||||
# TODO message typings here too
|
||||
data: Any # pyright: ignore [reportIncompatibleVariableOverride]
|
||||
type: WSMsgType # pyright: ignore [reportIncompatibleVariableOverride]
|
||||
|
||||
# see wsrouter.ts for typings
|
||||
|
||||
DataType = TypeVar("DataType")
|
||||
|
||||
Route = Callable[..., Coroutine[Any, Any, Any]]
|
||||
|
||||
class WSRouter:
|
||||
def __init__(self, loop: AbstractEventLoop, server_instance: Application) -> None:
|
||||
self.loop = loop
|
||||
self.ws: WebSocketResponse | None = None
|
||||
self.instance_id = 0
|
||||
self.routes: Dict[str, Route] = {}
|
||||
# self.subscriptions: Dict[str, Callable[[Any]]] = {}
|
||||
self.logger = getLogger("WSRouter")
|
||||
|
||||
server_instance.add_routes([
|
||||
get("/ws", self.handle)
|
||||
])
|
||||
|
||||
async def write(self, data: Dict[str, Any]):
|
||||
if self.ws != None:
|
||||
await self.ws.send_json(data)
|
||||
else:
|
||||
self.logger.warn("Dropping message as there is no connected socket: %s", data)
|
||||
|
||||
def add_route(self, name: str, route: Route):
|
||||
self.routes[name] = route
|
||||
|
||||
def remove_route(self, name: str):
|
||||
del self.routes[name]
|
||||
|
||||
async def _call_route(self, route: str, args: ..., call_id: int):
|
||||
instance_id = self.instance_id
|
||||
error = None
|
||||
try:
|
||||
res = await self.routes[route](*args)
|
||||
except Exception as err:
|
||||
error = {"name":err.__class__.__name__, "message":str(err), "traceback":format_exc()}
|
||||
res = None
|
||||
|
||||
if instance_id != self.instance_id:
|
||||
try:
|
||||
self.logger.warn("Ignoring %s reply from stale instance %d with args %s and response %s", route, instance_id, args, res)
|
||||
except:
|
||||
self.logger.warn("Ignoring %s reply from stale instance %d (failed to log event data)", route, instance_id)
|
||||
finally:
|
||||
return
|
||||
|
||||
if error:
|
||||
await self.write({"type": MessageType.ERROR.value, "id": call_id, "error": error})
|
||||
else:
|
||||
await self.write({"type": MessageType.REPLY.value, "id": call_id, "result": res})
|
||||
|
||||
async def handle(self, request: Request):
|
||||
# Auth is a query param as JS WebSocket doesn't support headers
|
||||
if request.rel_url.query["auth"] != get_csrf_token():
|
||||
return Response(text='Forbidden', status=403)
|
||||
self.logger.debug('Websocket connection starting')
|
||||
ws = WebSocketResponse()
|
||||
await ws.prepare(request)
|
||||
self.instance_id += 1
|
||||
self.logger.debug('Websocket connection ready')
|
||||
|
||||
if self.ws != None:
|
||||
try:
|
||||
await self.ws.close()
|
||||
except:
|
||||
pass
|
||||
self.ws = None
|
||||
|
||||
self.ws = ws
|
||||
|
||||
try:
|
||||
async for msg in ws:
|
||||
msg = cast(WSMessageExtra, msg)
|
||||
|
||||
if msg.type == WSMsgType.TEXT:
|
||||
if msg.data == 'close':
|
||||
# TODO DO NOT RELY ON THIS!
|
||||
break
|
||||
else:
|
||||
data = msg.json()
|
||||
match data["type"]:
|
||||
case MessageType.CALL.value:
|
||||
# do stuff with the message
|
||||
if data["route"] in self.routes:
|
||||
self.logger.debug(f'Started PY call {data["route"]} ID {data["id"]}')
|
||||
create_task(self._call_route(data["route"], data["args"], data["id"]))
|
||||
else:
|
||||
error = {"error":f'Route {data["route"]} does not exist.', "name": "RouteNotFoundError", "traceback": None}
|
||||
create_task(self.write({"type": MessageType.ERROR.value, "id": data["id"], "error": error}))
|
||||
case _:
|
||||
self.logger.error("Unknown message type", data)
|
||||
finally:
|
||||
try:
|
||||
await ws.close()
|
||||
self.ws = None
|
||||
except:
|
||||
pass
|
||||
|
||||
self.logger.debug('Websocket connection closed')
|
||||
return ws
|
||||
|
||||
async def emit(self, event: str, *args: Any):
|
||||
self.logger.debug(f'Firing frontend event {event} with args {args}')
|
||||
await self.write({ "type": MessageType.EVENT.value, "event": event, "args": args })
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
# This file is needed to make the relative imports in src/ work properly.
|
||||
# This file is needed to make the relative imports in decky_loader/ work properly.
|
||||
if __name__ == "__main__":
|
||||
from src.main import main
|
||||
from decky_loader.main import main
|
||||
main()
|
||||
|
||||
Generated
+766
@@ -0,0 +1,766 @@
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.9.5"
|
||||
description = "Async http client/server framework (asyncio)"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-win32.whl", hash = "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb"},
|
||||
{file = "aiohttp-3.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-win32.whl", hash = "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6"},
|
||||
{file = "aiohttp-3.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-win32.whl", hash = "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59"},
|
||||
{file = "aiohttp-3.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-win32.whl", hash = "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da"},
|
||||
{file = "aiohttp-3.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-win32.whl", hash = "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09"},
|
||||
{file = "aiohttp-3.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1"},
|
||||
{file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiosignal = ">=1.1.2"
|
||||
async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""}
|
||||
attrs = ">=17.3.0"
|
||||
frozenlist = ">=1.1.1"
|
||||
multidict = ">=4.5,<7.0"
|
||||
yarl = ">=1.0,<2.0"
|
||||
|
||||
[package.extras]
|
||||
speedups = ["Brotli", "aiodns", "brotlicffi"]
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp-cors"
|
||||
version = "0.7.0"
|
||||
description = "CORS support for aiohttp"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "aiohttp-cors-0.7.0.tar.gz", hash = "sha256:4d39c6d7100fd9764ed1caf8cebf0eb01bf5e3f24e2e073fda6234bc48b19f5d"},
|
||||
{file = "aiohttp_cors-0.7.0-py3-none-any.whl", hash = "sha256:0451ba59fdf6909d0e2cd21e4c0a43752bc0703d33fc78ae94d9d9321710193e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = ">=1.1"
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp-jinja2"
|
||||
version = "1.6"
|
||||
description = "jinja2 template renderer for aiohttp.web (http server for asyncio)"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "aiohttp-jinja2-1.6.tar.gz", hash = "sha256:a3a7ff5264e5bca52e8ae547bbfd0761b72495230d438d05b6c0915be619b0e2"},
|
||||
{file = "aiohttp_jinja2-1.6-py3-none-any.whl", hash = "sha256:0df405ee6ad1b58e5a068a105407dc7dcc1704544c559f1938babde954f945c7"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = ">=3.9.0"
|
||||
jinja2 = ">=3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "aiosignal"
|
||||
version = "1.3.1"
|
||||
description = "aiosignal: a list of registered asynchronous callbacks"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"},
|
||||
{file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
frozenlist = ">=1.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "altgraph"
|
||||
version = "0.17.4"
|
||||
description = "Python graph (network) package"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff"},
|
||||
{file = "altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-timeout"
|
||||
version = "4.0.3"
|
||||
description = "Timeout context manager for asyncio programs"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
|
||||
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "23.2.0"
|
||||
description = "Classes Without Boilerplate"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"},
|
||||
{file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
|
||||
dev = ["attrs[tests]", "pre-commit"]
|
||||
docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
|
||||
tests = ["attrs[tests-no-zope]", "zope-interface"]
|
||||
tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"]
|
||||
tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2024.6.2"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"},
|
||||
{file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "frozenlist"
|
||||
version = "1.4.1"
|
||||
description = "A list-like structure which implements collections.abc.MutableSequence"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"},
|
||||
{file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"},
|
||||
{file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"},
|
||||
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"},
|
||||
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"},
|
||||
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"},
|
||||
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"},
|
||||
{file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"},
|
||||
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"},
|
||||
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"},
|
||||
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"},
|
||||
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"},
|
||||
{file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"},
|
||||
{file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"},
|
||||
{file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"},
|
||||
{file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"},
|
||||
{file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"},
|
||||
{file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"},
|
||||
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"},
|
||||
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"},
|
||||
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"},
|
||||
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"},
|
||||
{file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"},
|
||||
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"},
|
||||
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"},
|
||||
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"},
|
||||
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"},
|
||||
{file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"},
|
||||
{file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"},
|
||||
{file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"},
|
||||
{file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"},
|
||||
{file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"},
|
||||
{file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"},
|
||||
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"},
|
||||
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"},
|
||||
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"},
|
||||
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"},
|
||||
{file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"},
|
||||
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"},
|
||||
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"},
|
||||
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"},
|
||||
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"},
|
||||
{file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"},
|
||||
{file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"},
|
||||
{file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"},
|
||||
{file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"},
|
||||
{file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"},
|
||||
{file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"},
|
||||
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"},
|
||||
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"},
|
||||
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"},
|
||||
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"},
|
||||
{file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"},
|
||||
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"},
|
||||
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"},
|
||||
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"},
|
||||
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"},
|
||||
{file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"},
|
||||
{file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"},
|
||||
{file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"},
|
||||
{file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"},
|
||||
{file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"},
|
||||
{file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"},
|
||||
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"},
|
||||
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"},
|
||||
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"},
|
||||
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"},
|
||||
{file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"},
|
||||
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"},
|
||||
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"},
|
||||
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"},
|
||||
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"},
|
||||
{file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"},
|
||||
{file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"},
|
||||
{file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"},
|
||||
{file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"},
|
||||
{file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.7"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
|
||||
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.4"
|
||||
description = "A very fast and expressive template engine."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
|
||||
{file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=2.0"
|
||||
|
||||
[package.extras]
|
||||
i18n = ["Babel (>=2.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "macholib"
|
||||
version = "1.16.3"
|
||||
description = "Mach-O header analysis and editing"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c"},
|
||||
{file = "macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
altgraph = ">=0.17"
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "2.1.5"
|
||||
description = "Safely add untrusted strings to HTML/XML markup."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"},
|
||||
{file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"},
|
||||
{file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"},
|
||||
{file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"},
|
||||
{file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"},
|
||||
{file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"},
|
||||
{file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"},
|
||||
{file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multidict"
|
||||
version = "6.0.5"
|
||||
description = "multidict implementation"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"},
|
||||
{file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"},
|
||||
{file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"},
|
||||
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"},
|
||||
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"},
|
||||
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"},
|
||||
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"},
|
||||
{file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"},
|
||||
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"},
|
||||
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"},
|
||||
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"},
|
||||
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"},
|
||||
{file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"},
|
||||
{file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"},
|
||||
{file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"},
|
||||
{file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"},
|
||||
{file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"},
|
||||
{file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"},
|
||||
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"},
|
||||
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"},
|
||||
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"},
|
||||
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"},
|
||||
{file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"},
|
||||
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"},
|
||||
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"},
|
||||
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"},
|
||||
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"},
|
||||
{file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"},
|
||||
{file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"},
|
||||
{file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"},
|
||||
{file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"},
|
||||
{file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"},
|
||||
{file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"},
|
||||
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"},
|
||||
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"},
|
||||
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"},
|
||||
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"},
|
||||
{file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"},
|
||||
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"},
|
||||
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"},
|
||||
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"},
|
||||
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"},
|
||||
{file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"},
|
||||
{file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"},
|
||||
{file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"},
|
||||
{file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"},
|
||||
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"},
|
||||
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"},
|
||||
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"},
|
||||
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"},
|
||||
{file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"},
|
||||
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"},
|
||||
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"},
|
||||
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"},
|
||||
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"},
|
||||
{file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"},
|
||||
{file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"},
|
||||
{file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"},
|
||||
{file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"},
|
||||
{file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"},
|
||||
{file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"},
|
||||
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"},
|
||||
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"},
|
||||
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"},
|
||||
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"},
|
||||
{file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"},
|
||||
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"},
|
||||
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"},
|
||||
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"},
|
||||
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"},
|
||||
{file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"},
|
||||
{file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"},
|
||||
{file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"},
|
||||
{file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"},
|
||||
{file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"},
|
||||
{file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"},
|
||||
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"},
|
||||
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"},
|
||||
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"},
|
||||
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"},
|
||||
{file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"},
|
||||
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"},
|
||||
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"},
|
||||
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"},
|
||||
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"},
|
||||
{file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"},
|
||||
{file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"},
|
||||
{file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"},
|
||||
{file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"},
|
||||
{file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodeenv"
|
||||
version = "1.9.1"
|
||||
description = "Node.js virtual environment builder"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
files = [
|
||||
{file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
|
||||
{file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.1"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
|
||||
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pefile"
|
||||
version = "2023.2.7"
|
||||
description = "Python PE parsing module"
|
||||
optional = false
|
||||
python-versions = ">=3.6.0"
|
||||
files = [
|
||||
{file = "pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"},
|
||||
{file = "pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller"
|
||||
version = "6.8.0"
|
||||
description = "PyInstaller bundles a Python application and all its dependencies into a single package."
|
||||
optional = false
|
||||
python-versions = "<3.13,>=3.8"
|
||||
files = [
|
||||
{file = "pyinstaller-6.8.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:5ff6bc2784c1026f8e2f04aa3760cbed41408e108a9d4cf1dd52ee8351a3f6e1"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:39ac424d2ee2457d2ab11a5091436e75a0cccae207d460d180aa1fcbbafdd528"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_i686.whl", hash = "sha256:355832a3acc7de90a255ecacd4b9f9e166a547a79c8905d49f14e3a75c1acdb9"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:6303c7a009f47e6a96ef65aed49f41e36ece8d079b9193ca92fe807403e5fe80"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2b71509468c811968c0b5decb5bbe85b6292ea52d7b1f26313d2aabb673fa9a5"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ff31c5b99e05a4384bbe2071df67ec8b2b347640a375eae9b40218be2f1754c6"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:000c36b13fe4cd8d0d8c2bc855b1ddcf39867b5adf389e6b5ca45b25fa3e619d"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:fe0af018d7d5077180e3144ada89a4da5df8d07716eb7e9482834a56dc57a4e8"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-win32.whl", hash = "sha256:d257f6645c7334cbd66f38a4fac62c3ad614cc46302b2b5d9f8cc48c563bce0e"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-win_amd64.whl", hash = "sha256:81cccfa9b16699b457f4788c5cc119b50f3cd4d0db924955f15c33f2ad27a50d"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-win_arm64.whl", hash = "sha256:1c3060a263758cf7f0144ab4c016097b20451b2469d468763414665db1bb743d"},
|
||||
{file = "pyinstaller-6.8.0.tar.gz", hash = "sha256:3f4b6520f4423fe19bcc2fd63ab7238851ae2bdcbc98f25bc5d2f97cc62012e9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
altgraph = "*"
|
||||
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
|
||||
packaging = ">=22.0"
|
||||
pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""}
|
||||
pyinstaller-hooks-contrib = ">=2024.6"
|
||||
pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""}
|
||||
setuptools = ">=42.0.0"
|
||||
|
||||
[package.extras]
|
||||
completion = ["argcomplete"]
|
||||
hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller-hooks-contrib"
|
||||
version = "2024.7"
|
||||
description = "Community maintained hooks for PyInstaller"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pyinstaller_hooks_contrib-2024.7-py2.py3-none-any.whl", hash = "sha256:8bf0775771fbaf96bcd2f4dfd6f7ae6c1dd1b1efe254c7e50477b3c08e7841d8"},
|
||||
{file = "pyinstaller_hooks_contrib-2024.7.tar.gz", hash = "sha256:fd5f37dcf99bece184e40642af88be16a9b89613ecb958a8bd1136634fc9fac5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
packaging = ">=22.0"
|
||||
setuptools = ">=42.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "pyright"
|
||||
version = "1.1.369"
|
||||
description = "Command line wrapper for pyright"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pyright-1.1.369-py3-none-any.whl", hash = "sha256:06d5167a8d7be62523ced0265c5d2f1e022e110caf57a25d92f50fb2d07bcda0"},
|
||||
{file = "pyright-1.1.369.tar.gz", hash = "sha256:ad290710072d021e213b98cc7a2f90ae3a48609ef5b978f749346d1a47eb9af8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
nodeenv = ">=1.6.0"
|
||||
|
||||
[package.extras]
|
||||
all = ["twine (>=3.4.1)"]
|
||||
dev = ["twine (>=3.4.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32-ctypes"
|
||||
version = "0.2.2"
|
||||
description = "A (partial) reimplementation of pywin32 using ctypes/cffi"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"},
|
||||
{file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "70.1.1"
|
||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "setuptools-70.1.1-py3-none-any.whl", hash = "sha256:a58a8fde0541dab0419750bcc521fbdf8585f6e5cb41909df3a472ef7b81ca95"},
|
||||
{file = "setuptools-70.1.1.tar.gz", hash = "sha256:937a48c7cdb7a21eb53cd7f9b59e525503aa8abaf3584c730dc5f7a5bec3a650"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
|
||||
testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
|
||||
|
||||
[[package]]
|
||||
name = "watchdog"
|
||||
version = "4.0.1"
|
||||
description = "Filesystem events monitoring"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645"},
|
||||
{file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b"},
|
||||
{file = "watchdog-4.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ef0107bbb6a55f5be727cfc2ef945d5676b97bffb8425650dadbb184be9f9a2b"},
|
||||
{file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:17e32f147d8bf9657e0922c0940bcde863b894cd871dbb694beb6704cfbd2fb5"},
|
||||
{file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03e70d2df2258fb6cb0e95bbdbe06c16e608af94a3ffbd2b90c3f1e83eb10767"},
|
||||
{file = "watchdog-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123587af84260c991dc5f62a6e7ef3d1c57dfddc99faacee508c71d287248459"},
|
||||
{file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:093b23e6906a8b97051191a4a0c73a77ecc958121d42346274c6af6520dec175"},
|
||||
{file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:611be3904f9843f0529c35a3ff3fd617449463cb4b73b1633950b3d97fa4bfb7"},
|
||||
{file = "watchdog-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:62c613ad689ddcb11707f030e722fa929f322ef7e4f18f5335d2b73c61a85c28"},
|
||||
{file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d4925e4bf7b9bddd1c3de13c9b8a2cdb89a468f640e66fbfabaf735bd85b3e35"},
|
||||
{file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cad0bbd66cd59fc474b4a4376bc5ac3fc698723510cbb64091c2a793b18654db"},
|
||||
{file = "watchdog-4.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a3c2c317a8fb53e5b3d25790553796105501a235343f5d2bf23bb8649c2c8709"},
|
||||
{file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9904904b6564d4ee8a1ed820db76185a3c96e05560c776c79a6ce5ab71888ba"},
|
||||
{file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:667f3c579e813fcbad1b784db7a1aaa96524bed53437e119f6a2f5de4db04235"},
|
||||
{file = "watchdog-4.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d10a681c9a1d5a77e75c48a3b8e1a9f2ae2928eda463e8d33660437705659682"},
|
||||
{file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0144c0ea9997b92615af1d94afc0c217e07ce2c14912c7b1a5731776329fcfc7"},
|
||||
{file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:998d2be6976a0ee3a81fb8e2777900c28641fb5bfbd0c84717d89bca0addcdc5"},
|
||||
{file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e7921319fe4430b11278d924ef66d4daa469fafb1da679a2e48c935fa27af193"},
|
||||
{file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f0de0f284248ab40188f23380b03b59126d1479cd59940f2a34f8852db710625"},
|
||||
{file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bca36be5707e81b9e6ce3208d92d95540d4ca244c006b61511753583c81c70dd"},
|
||||
{file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ab998f567ebdf6b1da7dc1e5accfaa7c6992244629c0fdaef062f43249bd8dee"},
|
||||
{file = "watchdog-4.0.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dddba7ca1c807045323b6af4ff80f5ddc4d654c8bce8317dde1bd96b128ed253"},
|
||||
{file = "watchdog-4.0.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:4513ec234c68b14d4161440e07f995f231be21a09329051e67a2118a7a612d2d"},
|
||||
{file = "watchdog-4.0.1-py3-none-manylinux2014_i686.whl", hash = "sha256:4107ac5ab936a63952dea2a46a734a23230aa2f6f9db1291bf171dac3ebd53c6"},
|
||||
{file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:6e8c70d2cd745daec2a08734d9f63092b793ad97612470a0ee4cbb8f5f705c57"},
|
||||
{file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f27279d060e2ab24c0aa98363ff906d2386aa6c4dc2f1a374655d4e02a6c5e5e"},
|
||||
{file = "watchdog-4.0.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:f8affdf3c0f0466e69f5b3917cdd042f89c8c63aebdb9f7c078996f607cdb0f5"},
|
||||
{file = "watchdog-4.0.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84"},
|
||||
{file = "watchdog-4.0.1-py3-none-win32.whl", hash = "sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429"},
|
||||
{file = "watchdog-4.0.1-py3-none-win_amd64.whl", hash = "sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a"},
|
||||
{file = "watchdog-4.0.1-py3-none-win_ia64.whl", hash = "sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d"},
|
||||
{file = "watchdog-4.0.1.tar.gz", hash = "sha256:eebaacf674fa25511e8867028d281e602ee6500045b57f43b08778082f7f8b44"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
watchmedo = ["PyYAML (>=3.10)"]
|
||||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
version = "1.9.4"
|
||||
description = "Yet another URL library"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"},
|
||||
{file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"},
|
||||
{file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"},
|
||||
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"},
|
||||
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"},
|
||||
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"},
|
||||
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"},
|
||||
{file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"},
|
||||
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"},
|
||||
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"},
|
||||
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"},
|
||||
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"},
|
||||
{file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"},
|
||||
{file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"},
|
||||
{file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"},
|
||||
{file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"},
|
||||
{file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"},
|
||||
{file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"},
|
||||
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"},
|
||||
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"},
|
||||
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"},
|
||||
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"},
|
||||
{file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"},
|
||||
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"},
|
||||
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"},
|
||||
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"},
|
||||
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"},
|
||||
{file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"},
|
||||
{file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"},
|
||||
{file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"},
|
||||
{file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"},
|
||||
{file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"},
|
||||
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"},
|
||||
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"},
|
||||
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"},
|
||||
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"},
|
||||
{file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"},
|
||||
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"},
|
||||
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"},
|
||||
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"},
|
||||
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"},
|
||||
{file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"},
|
||||
{file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"},
|
||||
{file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"},
|
||||
{file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"},
|
||||
{file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"},
|
||||
{file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"},
|
||||
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"},
|
||||
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"},
|
||||
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"},
|
||||
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"},
|
||||
{file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"},
|
||||
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"},
|
||||
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"},
|
||||
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"},
|
||||
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"},
|
||||
{file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"},
|
||||
{file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"},
|
||||
{file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"},
|
||||
{file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"},
|
||||
{file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"},
|
||||
{file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"},
|
||||
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"},
|
||||
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"},
|
||||
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"},
|
||||
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"},
|
||||
{file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"},
|
||||
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"},
|
||||
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"},
|
||||
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"},
|
||||
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"},
|
||||
{file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"},
|
||||
{file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"},
|
||||
{file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"},
|
||||
{file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"},
|
||||
{file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
idna = ">=2.0"
|
||||
multidict = ">=4.0"
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.10,<3.13"
|
||||
content-hash = "1d379cbadab535087a57eaa9ffc3d0af01b5274dac8df54080a39949c11f2003"
|
||||
@@ -0,0 +1,30 @@
|
||||
import os
|
||||
from PyInstaller.building.build_main import Analysis
|
||||
from PyInstaller.building.api import EXE, PYZ
|
||||
from PyInstaller.utils.hooks import copy_metadata
|
||||
|
||||
a = Analysis(
|
||||
['main.py'],
|
||||
datas=[
|
||||
('decky_loader/locales', 'decky_loader/locales'),
|
||||
('decky_loader/static', 'decky_loader/static'),
|
||||
] + copy_metadata('decky_loader'),
|
||||
hiddenimports=['logging.handlers', 'sqlite3', 'decky_plugin', 'decky'],
|
||||
)
|
||||
pyz = PYZ(a.pure, a.zipped_data)
|
||||
|
||||
noconsole = bool(os.getenv('DECKY_NOCONSOLE'))
|
||||
name = "PluginLoader"
|
||||
if noconsole:
|
||||
name += "_noconsole"
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
name=name,
|
||||
upx=True,
|
||||
console=not noconsole,
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
[tool.poetry]
|
||||
name = "decky-loader"
|
||||
version = "0.0.0" # the real version will be autogenerated
|
||||
description = "A plugin loader for the Steam Deck"
|
||||
license = "GPLv2"
|
||||
authors = []
|
||||
packages = [
|
||||
{include = "decky_loader"},
|
||||
{include = "decky_loader/main.py"}
|
||||
]
|
||||
include = [
|
||||
"decky_loader/locales/*",
|
||||
"decky_loader/static/*"
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.10,<3.13"
|
||||
|
||||
aiohttp = "^3.9.5"
|
||||
aiohttp-jinja2 = "^1.5.1"
|
||||
aiohttp-cors = "^0.7.0"
|
||||
watchdog = "^4"
|
||||
certifi = "*"
|
||||
packaging = "^24"
|
||||
multidict = "^6.0.5"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pyinstaller = "^6.8.0"
|
||||
pyright = "^1.1.335"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
decky-loader = 'decky_loader.main:main'
|
||||
|
||||
[tool.pyright]
|
||||
strict = ["*"]
|
||||
|
||||
[tool.poetry-dynamic-versioning]
|
||||
enable = true
|
||||
|
||||
[tool.poetry-dynamic-versioning.substitution]
|
||||
# don't replace version in decky_plugin.py
|
||||
files = []
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
|
||||
build-backend = "poetry_dynamic_versioning.backend"
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"strict": ["*"]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
aiohttp==3.9.0
|
||||
aiohttp-jinja2==1.5.1
|
||||
aiohttp_cors==0.7.0
|
||||
watchdog==2.1.7
|
||||
certifi==2023.7.22
|
||||
@@ -1,6 +0,0 @@
|
||||
from enum import Enum
|
||||
|
||||
class UserType(Enum):
|
||||
HOST_USER = 1
|
||||
EFFECTIVE_USER = 2
|
||||
ROOT = 3
|
||||
@@ -1,84 +0,0 @@
|
||||
class PluginEventTarget extends EventTarget { }
|
||||
method_call_ev_target = new PluginEventTarget();
|
||||
|
||||
window.addEventListener("message", function(evt) {
|
||||
let ev = new Event(evt.data.call_id);
|
||||
ev.data = evt.data.result;
|
||||
method_call_ev_target.dispatchEvent(ev);
|
||||
}, false);
|
||||
|
||||
async function call_server_method(method_name, arg_object={}) {
|
||||
const token = await fetch("http://127.0.0.1:1337/auth/token").then(r => r.text());
|
||||
const response = await fetch(`http://127.0.0.1:1337/methods/${method_name}`, {
|
||||
method: 'POST',
|
||||
credentials: "include",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authentication: token
|
||||
},
|
||||
body: JSON.stringify(arg_object),
|
||||
});
|
||||
|
||||
const dta = await response.json();
|
||||
if (!dta.success) throw dta.result;
|
||||
return dta.result;
|
||||
}
|
||||
|
||||
// Source: https://stackoverflow.com/a/2117523 Thanks!
|
||||
function uuidv4() {
|
||||
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
|
||||
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||
);
|
||||
}
|
||||
|
||||
async function fetch_nocors(url, request={}) {
|
||||
let args = { method: "POST", headers: {}, body: "" };
|
||||
request = {...args, ...request};
|
||||
request.url = url;
|
||||
request.data = request.body;
|
||||
delete request.body; //maintain api-compatibility with fetch
|
||||
return await call_server_method("http_request", request);
|
||||
}
|
||||
|
||||
async function call_plugin_method(method_name, arg_object={}) {
|
||||
if (plugin_name == undefined)
|
||||
throw new Error("Plugin methods can only be called from inside plugins (duh)");
|
||||
const token = await fetch("http://127.0.0.1:1337/auth/token").then(r => r.text());
|
||||
const response = await fetch(`http://127.0.0.1:1337/plugins/${plugin_name}/methods/${method_name}`, {
|
||||
method: 'POST',
|
||||
credentials: "include",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authentication: token
|
||||
},
|
||||
body: JSON.stringify({
|
||||
args: arg_object,
|
||||
}),
|
||||
});
|
||||
|
||||
const dta = await response.json();
|
||||
if (!dta.success) throw dta.result;
|
||||
return dta.result;
|
||||
}
|
||||
|
||||
async function execute_in_tab(tab, run_async, code) {
|
||||
return await call_server_method("execute_in_tab", {
|
||||
'tab': tab,
|
||||
'run_async': run_async,
|
||||
'code': code
|
||||
});
|
||||
}
|
||||
|
||||
async function inject_css_into_tab(tab, style) {
|
||||
return await call_server_method("inject_css_into_tab", {
|
||||
'tab': tab,
|
||||
'style': style
|
||||
});
|
||||
}
|
||||
|
||||
async function remove_css_from_tab(tab, css_id) {
|
||||
return await call_server_method("remove_css_from_tab", {
|
||||
'tab': tab,
|
||||
'css_id': css_id
|
||||
});
|
||||
}
|
||||
Vendored
+2
-4
@@ -34,16 +34,14 @@ curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/di
|
||||
cat > "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" <<- EOM
|
||||
[Unit]
|
||||
Description=SteamDeck Plugin Loader
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Restart=always
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
KillSignal=SIGKILL
|
||||
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
|
||||
Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=PRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=LOG_LEVEL=DEBUG
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
Vendored
+2
-4
@@ -34,16 +34,14 @@ curl -L https://raw.githubusercontent.com/SteamDeckHomebrew/decky-loader/main/di
|
||||
cat > "${HOMEBREW_FOLDER}/services/plugin_loader-backup.service" <<- EOM
|
||||
[Unit]
|
||||
Description=SteamDeck Plugin Loader
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Restart=always
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
KillSignal=SIGKILL
|
||||
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
|
||||
Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=PRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=LOG_LEVEL=INFO
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
-3
@@ -1,14 +1,11 @@
|
||||
[Unit]
|
||||
Description=SteamDeck Plugin Loader
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Restart=always
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
KillSignal=SIGKILL
|
||||
Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=PRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=LOG_LEVEL=DEBUG
|
||||
|
||||
Vendored
-3
@@ -1,14 +1,11 @@
|
||||
[Unit]
|
||||
Description=SteamDeck Plugin Loader
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Restart=always
|
||||
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||
KillSignal=SIGKILL
|
||||
Environment=UNPRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=PRIVILEGED_PATH=${HOMEBREW_FOLDER}
|
||||
Environment=LOG_LEVEL=INFO
|
||||
|
||||
Generated
+175
@@ -0,0 +1,175 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-github-actions": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"poetry2nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1703863825,
|
||||
"narHash": "sha256-rXwqjtwiGKJheXB43ybM8NwWB8rO2dSRrEqes0S7F5Y=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nix-github-actions",
|
||||
"rev": "5163432afc817cf8bd1f031418d1869e4c9d5547",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nix-github-actions",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1719254875,
|
||||
"narHash": "sha256-ECni+IkwXjusHsm9Sexdtq8weAq/yUyt1TWIemXt3Ko=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "2893f56de08021cffd9b6b6dfc70fd9ccd51eb60",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"poetry2nix": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nix-github-actions": "nix-github-actions",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"systems": "systems_3",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1719395064,
|
||||
"narHash": "sha256-SsutCU+IytywS9HHGKtVZaGINcm6lpHXcyJzy7Rv0Co=",
|
||||
"owner": "nix-community",
|
||||
"repo": "poetry2nix",
|
||||
"rev": "0e52508053e3dcb568bf432a144bff367978d199",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "poetry2nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"poetry2nix": "poetry2nix"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_3": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "systems",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"poetry2nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1718522839,
|
||||
"narHash": "sha256-ULzoKzEaBOiLRtjeY3YoGFJMwWSKRYOic6VNw2UyTls=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "68eb1dc333ce82d0ab0c0357363ea17c31ea1f81",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
description = "Decky development environment";
|
||||
# pulls in the python deps from poetry
|
||||
|
||||
inputs = {
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
poetry2nix = {
|
||||
url = "github:nix-community/poetry2nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, poetry2nix }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
p2n = (poetry2nix.lib.mkPoetry2Nix { inherit pkgs; });
|
||||
in {
|
||||
devShells.default = (p2n.mkPoetryEnv {
|
||||
projectDir = self + "/backend";
|
||||
# pyinstaller fails to compile so precompiled it is
|
||||
overrides = p2n.overrides.withDefaults (final: prev: {
|
||||
pyinstaller = prev.pyinstaller.override { preferWheel = true; };
|
||||
pyright = null;
|
||||
});
|
||||
}).env.overrideAttrs (oldAttrs: {
|
||||
shellHook = ''
|
||||
set -o noclobber
|
||||
PYTHONPATH=`which python`
|
||||
FILE=.vscode/settings.json
|
||||
if [ -f "$FILE" ]; then
|
||||
echo "$FILE already exists, not writing interpreter path to it."
|
||||
else
|
||||
echo "{\"python.defaultInterpreterPath\": \"''${PYTHONPATH}\"}" > "$FILE"
|
||||
fi
|
||||
'';
|
||||
UV_USE_IO_URING = 0; # work around node#48444
|
||||
buildInputs = with pkgs; [
|
||||
nodejs_22
|
||||
nodePackages.pnpm
|
||||
poetry
|
||||
# fixes local pyright not being able to see the pythonpath properly.
|
||||
(pkgs.writeShellScriptBin "pyright" ''
|
||||
${pkgs.pyright}/bin/pyright --pythonpath `which python3` "$@" '')
|
||||
(pkgs.writeShellScriptBin "pyright-langserver" ''
|
||||
${pkgs.pyright}/bin/pyright-langserver --pythonpath `which python3` "$@" '')
|
||||
(pkgs.writeShellScriptBin "pyright-python" ''
|
||||
${pkgs.pyright}/bin/pyright-python --pythonpath `which python3` "$@" '')
|
||||
(pkgs.writeShellScriptBin "pyright-python-langserver" ''
|
||||
${pkgs.pyright}/bin/pyright-python-langserver --pythonpath `which python3` "$@" '')
|
||||
];
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
module.exports = {
|
||||
import importSort from 'prettier-plugin-import-sort';
|
||||
export default {
|
||||
semi: true,
|
||||
trailingComma: 'all',
|
||||
singleQuote: true,
|
||||
printWidth: 120,
|
||||
tabWidth: 2,
|
||||
endOfLine: 'auto',
|
||||
plugins: [require('prettier-plugin-import-sort')],
|
||||
};
|
||||
plugins: [importSort],
|
||||
}
|
||||
@@ -43,7 +43,7 @@ export default {
|
||||
// Namespace separator used in your translation keys
|
||||
// If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance.
|
||||
|
||||
output: '../backend/locales/$LOCALE.json',
|
||||
output: '../backend/decky_loader/locales/$LOCALE.json',
|
||||
// Supports $LOCALE and $NAMESPACE injection
|
||||
// Supports JSON (.json) and YAML (.yml) file formats
|
||||
// Where to write the locale files relative to process.cwd()
|
||||
|
||||
+35
-33
@@ -1,41 +1,43 @@
|
||||
{
|
||||
"name": "decky_frontend",
|
||||
"version": "2.1.1",
|
||||
"name": "@decky/loader-frontend",
|
||||
"private": true,
|
||||
"license": "GPLV2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"prepare": "cd .. && husky install frontend/.husky",
|
||||
"build": "rollup -c",
|
||||
"watch": "rollup -c -w",
|
||||
"lint": "prettier -c src",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"format": "prettier -c src -w"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^21.1.0",
|
||||
"@rollup/plugin-image": "^3.0.2",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^13.3.0",
|
||||
"@rollup/plugin-replace": "^4.0.0",
|
||||
"@rollup/plugin-typescript": "^8.5.0",
|
||||
"@types/react": "16.14.0",
|
||||
"@types/react-file-icon": "^1.0.1",
|
||||
"@types/react-router": "5.1.18",
|
||||
"@types/webpack": "^5.28.1",
|
||||
"husky": "^8.0.3",
|
||||
"i18next-parser": "^8.0.0",
|
||||
"@decky/api": "^1.0.5",
|
||||
"@rollup/plugin-commonjs": "^26.0.1",
|
||||
"@rollup/plugin-image": "^3.0.3",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-replace": "^5.0.7",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@types/react-file-icon": "^1.0.4",
|
||||
"@types/react-router": "5.1.20",
|
||||
"husky": "^9.0.11",
|
||||
"i18next-parser": "^9.0.0",
|
||||
"import-sort-style-module": "^6.0.0",
|
||||
"inquirer": "^8.2.5",
|
||||
"prettier": "^3.2.5",
|
||||
"inquirer": "^9.2.23",
|
||||
"prettier": "^3.3.2",
|
||||
"prettier-plugin-import-sort": "^0.0.7",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"rollup": "^2.79.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"rollup": "^4.18.0",
|
||||
"rollup-plugin-delete": "^2.0.0",
|
||||
"rollup-plugin-external-globals": "^0.6.1",
|
||||
"rollup-plugin-polyfill-node": "^0.10.2",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"tslib": "^2.5.3",
|
||||
"typescript": "^4.9.5"
|
||||
"rollup-plugin-external-globals": "^0.10.0",
|
||||
"rollup-plugin-polyfill-node": "^0.13.0",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"tslib": "^2.6.3",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"importSort": {
|
||||
".js, .jsx, .ts, .tsx": {
|
||||
@@ -44,14 +46,14 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"decky-frontend-lib": "3.25.0",
|
||||
"filesize": "^10.0.7",
|
||||
"i18next": "^23.2.1",
|
||||
"i18next-http-backend": "^2.2.1",
|
||||
"react-file-icon": "^1.3.0",
|
||||
"react-i18next": "^12.3.1",
|
||||
"react-icons": "^4.9.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"remark-gfm": "^3.0.1"
|
||||
"@decky/ui": "^4.2.1",
|
||||
"filesize": "^10.1.2",
|
||||
"i18next": "^23.11.5",
|
||||
"i18next-http-backend": "^2.5.2",
|
||||
"react-file-icon": "^1.5.0",
|
||||
"react-i18next": "^14.1.2",
|
||||
"react-icons": "^5.2.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"remark-gfm": "^4.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1657
-1847
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ const hiddenWarnings = ['THIS_IS_UNDEFINED', 'EVAL'];
|
||||
export default defineConfig({
|
||||
input: 'src/index.ts',
|
||||
plugins: [
|
||||
del({ targets: '../backend/static/*', force: true }),
|
||||
del({ targets: '../backend/decky_loader/static/*', force: true }),
|
||||
commonjs(),
|
||||
nodeResolve({
|
||||
browser: true,
|
||||
@@ -38,11 +38,13 @@ export default defineConfig({
|
||||
],
|
||||
preserveEntrySignatures: false,
|
||||
output: {
|
||||
dir: '../backend/static',
|
||||
dir: '../backend/decky_loader/static',
|
||||
format: 'esm',
|
||||
chunkFileNames: (chunkInfo) => {
|
||||
return 'chunk-[hash].js';
|
||||
},
|
||||
sourcemap: true,
|
||||
sourcemapPathTransform: (relativeSourcePath) => relativeSourcePath.replace(/^\.\.\//, `decky://decky/loader/`),
|
||||
},
|
||||
onwarn: function (message, handleWarning) {
|
||||
if (hiddenWarnings.some((warning) => message.code === warning)) return;
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
import { sleep } from '@decky/ui';
|
||||
import { FunctionComponent, useEffect, useReducer, useState } from 'react';
|
||||
|
||||
import { uninstallPlugin } from '../plugin';
|
||||
import { VerInfo, doRestart, doShutdown } from '../updater';
|
||||
import { ValveReactErrorInfo, getLikelyErrorSourceFromValveReactError } from '../utils/errors';
|
||||
|
||||
interface DeckyErrorBoundaryProps {
|
||||
error: ValveReactErrorInfo;
|
||||
errorKey: string;
|
||||
identifier: string;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
SystemNetworkStore?: any;
|
||||
}
|
||||
}
|
||||
|
||||
export const startSSH = DeckyBackend.callable('utilities/start_ssh');
|
||||
export const starrCEFForwarding = DeckyBackend.callable('utilities/allow_remote_debugging');
|
||||
|
||||
function ipToString(ip: number) {
|
||||
return [(ip >>> 24) & 255, (ip >>> 16) & 255, (ip >>> 8) & 255, (ip >>> 0) & 255].join('.');
|
||||
}
|
||||
|
||||
// Intentionally not localized since we can't really trust React here
|
||||
const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error, identifier, reset }) => {
|
||||
const [actionLog, addLogLine] = useReducer((log: string, line: string) => (log += '\n' + line), '');
|
||||
const [actionsEnabled, setActionsEnabled] = useState<boolean>(true);
|
||||
const [debugAllowed, setDebugAllowed] = useState<boolean>(true);
|
||||
// Intentionally doesn't use DeckyState.
|
||||
const [versionInfo, setVersionInfo] = useState<VerInfo>();
|
||||
const [errorSource, wasCausedByPlugin, shouldReportToValve] = getLikelyErrorSourceFromValveReactError(error);
|
||||
useEffect(() => {
|
||||
if (!shouldReportToValve) DeckyPluginLoader.errorBoundaryHook.temporarilyDisableReporting();
|
||||
DeckyPluginLoader.updateVersion().then(setVersionInfo);
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
*:has(> .deckyErrorBoundary) {
|
||||
overflow: scroll !important;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div
|
||||
style={{
|
||||
overflow: 'auto',
|
||||
marginLeft: '15px',
|
||||
color: 'white',
|
||||
fontSize: '16px',
|
||||
userSelect: 'auto',
|
||||
backgroundColor: 'black',
|
||||
marginTop: '48px', // Incase this is a page
|
||||
}}
|
||||
className="deckyErrorBoundary"
|
||||
>
|
||||
<h1
|
||||
style={{
|
||||
fontSize: '20px',
|
||||
display: 'inline-block',
|
||||
userSelect: 'auto',
|
||||
}}
|
||||
>
|
||||
⚠️ An error occured while rendering this content.
|
||||
</h1>
|
||||
<pre style={{}}>
|
||||
<code>
|
||||
{identifier && `Error Reference: ${identifier}`}
|
||||
{versionInfo?.current && `\nDecky Version: ${versionInfo.current}`}
|
||||
</code>
|
||||
</pre>
|
||||
<p>This error likely occured in {errorSource}.</p>
|
||||
{actionLog?.length > 0 && (
|
||||
<pre>
|
||||
<code>
|
||||
Running actions...
|
||||
{actionLog}
|
||||
</code>
|
||||
</pre>
|
||||
)}
|
||||
{actionsEnabled && (
|
||||
<>
|
||||
<h3>Actions: </h3>
|
||||
<p>Use the touch screen.</p>
|
||||
<div style={{ display: 'block', marginBottom: '5px' }}>
|
||||
<button style={{ marginRight: '5px', padding: '5px' }} onClick={reset}>
|
||||
Retry
|
||||
</button>
|
||||
<button
|
||||
style={{ marginRight: '5px', padding: '5px' }}
|
||||
onClick={() => {
|
||||
addLogLine('Restarting Steam...');
|
||||
SteamClient.User.StartRestart();
|
||||
}}
|
||||
>
|
||||
Restart Steam
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: 'block', marginBottom: '5px' }}>
|
||||
<button
|
||||
style={{ marginRight: '5px', padding: '5px' }}
|
||||
onClick={async () => {
|
||||
setActionsEnabled(false);
|
||||
addLogLine('Restarting Decky...');
|
||||
doRestart();
|
||||
await sleep(2000);
|
||||
addLogLine('Reloading UI...');
|
||||
}}
|
||||
>
|
||||
Restart Decky
|
||||
</button>
|
||||
<button
|
||||
style={{ marginRight: '5px', padding: '5px' }}
|
||||
onClick={async () => {
|
||||
setActionsEnabled(false);
|
||||
addLogLine('Stopping Decky...');
|
||||
doShutdown();
|
||||
await sleep(5000);
|
||||
addLogLine('Restarting Steam...');
|
||||
SteamClient.User.StartRestart();
|
||||
}}
|
||||
>
|
||||
Disable Decky until next boot
|
||||
</button>
|
||||
</div>
|
||||
{debugAllowed && (
|
||||
<div style={{ display: 'block', marginBottom: '5px' }}>
|
||||
<button
|
||||
style={{ marginRight: '5px', padding: '5px' }}
|
||||
onClick={async () => {
|
||||
setDebugAllowed(false);
|
||||
addLogLine('Enabling CEF debugger forwarding...');
|
||||
await starrCEFForwarding();
|
||||
addLogLine('Enabling SSH...');
|
||||
await startSSH();
|
||||
addLogLine('Ready for debugging!');
|
||||
if (window?.SystemNetworkStore?.wirelessNetworkDevice?.ip4?.addresses?.[0]?.ip) {
|
||||
const ip = ipToString(window.SystemNetworkStore.wirelessNetworkDevice.ip4.addresses[0].ip);
|
||||
addLogLine(`CEF Debugger: http://${ip}:8081`);
|
||||
addLogLine(`SSH: deck@${ip}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Allow remote debugging and SSH until next boot
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{wasCausedByPlugin && (
|
||||
<div style={{ display: 'block', marginBottom: '5px' }}>
|
||||
{'\n'}
|
||||
<button
|
||||
style={{ marginRight: '5px', padding: '5px' }}
|
||||
onClick={async () => {
|
||||
setActionsEnabled(false);
|
||||
addLogLine(`Uninstalling ${errorSource}...`);
|
||||
await uninstallPlugin(errorSource);
|
||||
await DeckyPluginLoader.frozenPluginsService.invalidate();
|
||||
await DeckyPluginLoader.hiddenPluginsService.invalidate();
|
||||
await sleep(1000);
|
||||
addLogLine('Restarting Decky...');
|
||||
doRestart();
|
||||
await sleep(2000);
|
||||
addLogLine('Restarting Steam...');
|
||||
await sleep(500);
|
||||
SteamClient.User.StartRestart();
|
||||
}}
|
||||
>
|
||||
Uninstall {errorSource} and restart Decky
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<pre
|
||||
style={{
|
||||
marginTop: '15px',
|
||||
opacity: 0.7,
|
||||
userSelect: 'auto',
|
||||
}}
|
||||
>
|
||||
<code>
|
||||
{error.error.stack}
|
||||
{'\n\n'}
|
||||
Component Stack:
|
||||
{error.info.componentStack}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeckyErrorBoundary;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, createContext, useContext, useEffect, useState } from 'react';
|
||||
import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
interface PublicDeckyGlobalComponentsState {
|
||||
components: Map<string, FC>;
|
||||
@@ -40,6 +40,7 @@ export const useDeckyGlobalComponentsState = () => useContext(DeckyGlobalCompone
|
||||
|
||||
interface Props {
|
||||
deckyGlobalComponentsState: DeckyGlobalComponentsState;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const DeckyGlobalComponentsStateContextProvider: FC<Props> = ({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentType, FC, createContext, useContext, useEffect, useState } from 'react';
|
||||
import { ComponentType, FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
|
||||
import type { RouteProps } from 'react-router';
|
||||
|
||||
export interface RouterEntry {
|
||||
@@ -71,6 +71,7 @@ export const useDeckyRouterState = () => useContext(DeckyRouterStateContext);
|
||||
|
||||
interface Props {
|
||||
deckyRouterState: DeckyRouterState;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const DeckyRouterStateContextProvider: FC<Props> = ({ children, deckyRouterState }) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, createContext, useContext, useEffect, useState } from 'react';
|
||||
import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { DEFAULT_NOTIFICATION_SETTINGS, NotificationSettings } from '../notification-service';
|
||||
import { Plugin } from '../plugin';
|
||||
@@ -134,6 +134,7 @@ export const useDeckyState = () => useContext(DeckyStateContext);
|
||||
|
||||
interface Props {
|
||||
deckyState: DeckyState;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ToastData, joinClassNames } from 'decky-frontend-lib';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { ReactElement } from 'react-markdown/lib/react-markdown';
|
||||
import type { ToastData } from '@decky/api';
|
||||
import { joinClassNames } from '@decky/ui';
|
||||
import { FC, ReactElement, useEffect, useState } from 'react';
|
||||
|
||||
import { useDeckyToasterState } from './DeckyToasterState';
|
||||
import Toast, { toastClasses } from './Toast';
|
||||
@@ -19,7 +19,7 @@ const DeckyToaster: FC<DeckyToasterProps> = () => {
|
||||
if (toasts.size > 0) {
|
||||
const [activeToast] = toasts;
|
||||
if (!renderedToast || activeToast != renderedToast.data) {
|
||||
// TODO play toast sound
|
||||
// TODO play toast soundReactElement
|
||||
console.log('rendering toast', activeToast);
|
||||
setRenderedToast({ component: <Toast key={Math.random()} toast={activeToast} />, data: activeToast });
|
||||
}
|
||||
@@ -28,7 +28,7 @@ const DeckyToaster: FC<DeckyToasterProps> = () => {
|
||||
}
|
||||
useEffect(() => {
|
||||
// not actually node but TS is shit
|
||||
let interval: NodeJS.Timer | null;
|
||||
let interval: number | null;
|
||||
if (renderedToast) {
|
||||
interval = setTimeout(
|
||||
() => {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { ToastData } from 'decky-frontend-lib';
|
||||
import { FC, createContext, useContext, useEffect, useState } from 'react';
|
||||
import type { ToastData } from '@decky/api';
|
||||
import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
interface PublicDeckyToasterState {
|
||||
toasts: Set<ToastData>;
|
||||
}
|
||||
|
||||
export class DeckyToasterState {
|
||||
// TODO a set would be better
|
||||
private _toasts: Set<ToastData> = new Set();
|
||||
|
||||
public eventBus = new EventTarget();
|
||||
@@ -41,6 +40,7 @@ export const useDeckyToasterState = () => useContext(DeckyToasterContext);
|
||||
|
||||
interface Props {
|
||||
deckyToasterState: DeckyToasterState;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const DeckyToasterStateContextProvider: FC<Props> = ({ children, deckyToasterState }) => {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { VFC } from 'react';
|
||||
|
||||
interface Props {
|
||||
url: string;
|
||||
}
|
||||
|
||||
const LegacyPlugin: VFC<Props> = ({ url }) => {
|
||||
return <iframe style={{ border: 'none', width: '100%', height: '100%' }} src={url}></iframe>;
|
||||
};
|
||||
|
||||
export default LegacyPlugin;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Focusable, Navigation } from 'decky-frontend-lib';
|
||||
import { Focusable, Navigation } from '@decky/ui';
|
||||
import { FunctionComponent, useRef } from 'react';
|
||||
import ReactMarkdown, { Options as ReactMarkdownOptions } from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
@@ -13,8 +13,8 @@ const Markdown: FunctionComponent<MarkdownProps> = (props) => {
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
div: (nodeProps) => <Focusable {...nodeProps.node.properties}>{nodeProps.children}</Focusable>,
|
||||
a: (nodeProps) => {
|
||||
div: (nodeProps: any) => <Focusable {...nodeProps.node.properties}>{nodeProps.children}</Focusable>,
|
||||
a: (nodeProps: any) => {
|
||||
const aRef = useRef<HTMLAnchorElement>(null);
|
||||
return (
|
||||
// TODO fix focus ring
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ButtonItem, Focusable, PanelSection, PanelSectionRow } from 'decky-frontend-lib';
|
||||
import { VFC, useEffect, useState } from 'react';
|
||||
import { ButtonItem, ErrorBoundary, Focusable, PanelSection, PanelSectionRow } from '@decky/ui';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaEyeSlash } from 'react-icons/fa';
|
||||
|
||||
@@ -9,7 +9,7 @@ import NotificationBadge from './NotificationBadge';
|
||||
import { useQuickAccessVisible } from './QuickAccessVisibleState';
|
||||
import TitleView from './TitleView';
|
||||
|
||||
const PluginView: VFC = () => {
|
||||
const PluginView: FC = () => {
|
||||
const { hiddenPlugins } = useDeckyState();
|
||||
const { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState();
|
||||
const visible = useQuickAccessVisible();
|
||||
@@ -29,7 +29,7 @@ const PluginView: VFC = () => {
|
||||
<Focusable onCancelButton={closeActivePlugin}>
|
||||
<TitleView />
|
||||
<div style={{ height: '100%', paddingTop: '16px' }}>
|
||||
{(visible || activePlugin.alwaysRender) && activePlugin.content}
|
||||
<ErrorBoundary>{(visible || activePlugin.alwaysRender) && activePlugin.content}</ErrorBoundary>
|
||||
</div>
|
||||
</Focusable>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { FC, createContext, useContext, useState } from 'react';
|
||||
import { FC, ReactNode, createContext, useContext, useState } from 'react';
|
||||
|
||||
const QuickAccessVisibleState = createContext<boolean>(false);
|
||||
|
||||
export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState);
|
||||
|
||||
export const QuickAccessVisibleStateProvider: FC<{ tab: any }> = ({ children, tab }) => {
|
||||
export const QuickAccessVisibleStateProvider: FC<{ tab: any; children: ReactNode }> = ({ children, tab }) => {
|
||||
const initial = tab.initialVisibility;
|
||||
const [visible, setVisible] = useState<boolean>(initial);
|
||||
// HACK but i can't think of a better way to do this
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DialogButton, Focusable, Router, staticClasses } from 'decky-frontend-lib';
|
||||
import { CSSProperties, VFC } from 'react';
|
||||
import { DialogButton, Focusable, Router, staticClasses } from '@decky/ui';
|
||||
import { CSSProperties, FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BsGearFill } from 'react-icons/bs';
|
||||
import { FaArrowLeft, FaStore } from 'react-icons/fa';
|
||||
@@ -14,7 +14,7 @@ const titleStyles: CSSProperties = {
|
||||
top: '0px',
|
||||
};
|
||||
|
||||
const TitleView: VFC = () => {
|
||||
const TitleView: FC = () => {
|
||||
const { activePlugin, closeActivePlugin } = useDeckyState();
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ToastData, findModule, joinClassNames } from 'decky-frontend-lib';
|
||||
import type { ToastData } from '@decky/api';
|
||||
import { findModule, joinClassNames } from '@decky/ui';
|
||||
import { FunctionComponent } from 'react';
|
||||
|
||||
interface ToastProps {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Focusable, SteamSpinner } from 'decky-frontend-lib';
|
||||
import { Focusable, SteamSpinner } from '@decky/ui';
|
||||
import { FunctionComponent, ReactElement, ReactNode, Suspense } from 'react';
|
||||
|
||||
interface WithSuspenseProps {
|
||||
|
||||
@@ -2,24 +2,21 @@ import {
|
||||
DialogButton,
|
||||
DialogCheckbox,
|
||||
DialogCheckboxProps,
|
||||
Export,
|
||||
Marquee,
|
||||
Menu,
|
||||
MenuItem,
|
||||
findModuleChild,
|
||||
findModuleExport,
|
||||
showContextMenu,
|
||||
} from 'decky-frontend-lib';
|
||||
} from '@decky/ui';
|
||||
import { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaChevronDown } from 'react-icons/fa';
|
||||
|
||||
const dropDownControlButtonClass = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (const prop in m) {
|
||||
if (m[prop]?.toString()?.includes('gamepaddropdown_DropDownControlButton')) {
|
||||
return m[prop];
|
||||
}
|
||||
}
|
||||
});
|
||||
// TODO add to dfl
|
||||
const dropDownControlButtonClass = findModuleExport((e: Export) =>
|
||||
e?.toString()?.includes('gamepaddropdown_DropDownControlButton'),
|
||||
);
|
||||
|
||||
const DropdownMultiselectItem: FC<
|
||||
{
|
||||
@@ -62,7 +59,7 @@ const DropdownMultiselect: FC<{
|
||||
const [itemsSelected, setItemsSelected] = useState<any>(selected);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleItemSelect = useCallback((checked, value) => {
|
||||
const handleItemSelect = useCallback((checked: boolean, value: any) => {
|
||||
setItemsSelected((x: any) =>
|
||||
checked ? [...x.filter((y: any) => y !== value), value] : x.filter((y: any) => y !== value),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ConfirmModal, Navigation, QuickAccessTab } from 'decky-frontend-lib';
|
||||
import { FC, useMemo, useState } from 'react';
|
||||
import { ConfirmModal, Navigation, ProgressBarWithInfo, QuickAccessTab } from '@decky/ui';
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaCheck, FaDownload } from 'react-icons/fa';
|
||||
|
||||
import { InstallType } from '../../plugin';
|
||||
|
||||
@@ -27,8 +28,42 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
|
||||
closeModal,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [percentage, setPercentage] = useState<number>(0);
|
||||
const [pluginsCompleted, setPluginsCompleted] = useState<string[]>([]);
|
||||
const [pluginInProgress, setInProgress] = useState<string | null>();
|
||||
const [downloadInfo, setDownloadInfo] = useState<string | null>(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
function updateDownloadState(percent: number, trans_text: string | undefined, trans_info: Record<string, string>) {
|
||||
setPercentage(percent);
|
||||
if (trans_text === undefined) {
|
||||
setDownloadInfo(null);
|
||||
} else {
|
||||
setDownloadInfo(t(trans_text, trans_info));
|
||||
}
|
||||
}
|
||||
|
||||
function startDownload(name: string) {
|
||||
setInProgress(name);
|
||||
setPercentage(0);
|
||||
}
|
||||
|
||||
function finishDownload(name: string) {
|
||||
setPluginsCompleted((list) => [...list, name]);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
DeckyBackend.addEventListener('loader/plugin_download_info', updateDownloadState);
|
||||
DeckyBackend.addEventListener('loader/plugin_download_start', startDownload);
|
||||
DeckyBackend.addEventListener('loader/plugin_download_finish', finishDownload);
|
||||
|
||||
return () => {
|
||||
DeckyBackend.removeEventListener('loader/plugin_download_info', updateDownloadState);
|
||||
DeckyBackend.removeEventListener('loader/plugin_download_start', startDownload);
|
||||
DeckyBackend.removeEventListener('loader/plugin_download_finish', finishDownload);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// used as part of the title translation
|
||||
// if we know all operations are of a specific type, we can show so in the title to make decision easier
|
||||
const installTypeGrouped = useMemo((): TitleTranslationMapping => {
|
||||
@@ -46,7 +81,7 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
|
||||
setLoading(true);
|
||||
await onOK();
|
||||
setTimeout(() => Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky), 250);
|
||||
setTimeout(() => window.DeckyPluginLoader.checkPluginUpdates(), 1000);
|
||||
setTimeout(() => DeckyPluginLoader.checkPluginUpdates(), 1000);
|
||||
}}
|
||||
onCancel={async () => {
|
||||
await onCancel();
|
||||
@@ -66,7 +101,10 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
|
||||
|
||||
return (
|
||||
<li key={i} style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<div>{description}</div>
|
||||
<span>
|
||||
{description}{' '}
|
||||
{(pluginsCompleted.includes(name) && <FaCheck />) || (name === pluginInProgress && <FaDownload />)}
|
||||
</span>
|
||||
{hash === 'False' && (
|
||||
<div style={{ color: 'red', paddingLeft: '10px' }}>{t('PluginInstallModal.no_hash')}</div>
|
||||
)}
|
||||
@@ -74,6 +112,17 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{/* TODO: center the progress bar and make it 80% width */}
|
||||
{loading && (
|
||||
<ProgressBarWithInfo
|
||||
// when the key changes, react considers this a new component so resets the progress without the smoothing animation
|
||||
key={pluginInProgress}
|
||||
bottomSeparator="none"
|
||||
focusable={false}
|
||||
nProgress={percentage}
|
||||
sOperationText={downloadInfo}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ConfirmModal>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ConfirmModal, Navigation, QuickAccessTab } from 'decky-frontend-lib';
|
||||
import { FC, useState } from 'react';
|
||||
import { ConfirmModal, Navigation, ProgressBarWithInfo, QuickAccessTab } from '@decky/ui';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import TranslationHelper, { TranslationClass } from '../../utils/TranslationHelper';
|
||||
@@ -24,8 +24,26 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
|
||||
closeModal,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [percentage, setPercentage] = useState<number>(0);
|
||||
const [downloadInfo, setDownloadInfo] = useState<string | null>(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
function updateDownloadState(percent: number, trans_text: string | undefined, trans_info: Record<string, string>) {
|
||||
setPercentage(percent);
|
||||
if (trans_text === undefined) {
|
||||
setDownloadInfo(null);
|
||||
} else {
|
||||
setDownloadInfo(t(trans_text, trans_info));
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
DeckyBackend.addEventListener('loader/plugin_download_info', updateDownloadState);
|
||||
return () => {
|
||||
DeckyBackend.removeEventListener('loader/plugin_download_info', updateDownloadState);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ConfirmModal
|
||||
bOKDisabled={loading}
|
||||
@@ -34,7 +52,7 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
|
||||
setLoading(true);
|
||||
await onOK();
|
||||
setTimeout(() => Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky), 250);
|
||||
setTimeout(() => window.DeckyPluginLoader.checkPluginUpdates(), 1000);
|
||||
setTimeout(() => DeckyPluginLoader.checkPluginUpdates(), 1000);
|
||||
}}
|
||||
onCancel={async () => {
|
||||
await onCancel();
|
||||
@@ -42,10 +60,10 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
|
||||
strTitle={
|
||||
<div>
|
||||
<TranslationHelper
|
||||
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
|
||||
trans_text="title"
|
||||
i18n_args={{ artifact: artifact }}
|
||||
install_type={installType}
|
||||
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
|
||||
transText="title"
|
||||
i18nArgs={{ artifact: artifact }}
|
||||
installType={installType}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
@@ -53,17 +71,17 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
|
||||
loading ? (
|
||||
<div>
|
||||
<TranslationHelper
|
||||
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
|
||||
trans_text="button_processing"
|
||||
install_type={installType}
|
||||
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
|
||||
transText="button_processing"
|
||||
installType={installType}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<TranslationHelper
|
||||
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
|
||||
trans_text="button_idle"
|
||||
install_type={installType}
|
||||
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
|
||||
transText="button_idle"
|
||||
installType={installType}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -71,15 +89,23 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
|
||||
>
|
||||
<div>
|
||||
<TranslationHelper
|
||||
trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
|
||||
trans_text="desc"
|
||||
i18n_args={{
|
||||
transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
|
||||
transText="desc"
|
||||
i18nArgs={{
|
||||
artifact: artifact,
|
||||
version: version,
|
||||
}}
|
||||
install_type={installType}
|
||||
installType={installType}
|
||||
/>
|
||||
</div>
|
||||
{loading && (
|
||||
<ProgressBarWithInfo
|
||||
layout="inline"
|
||||
bottomSeparator="none"
|
||||
nProgress={percentage}
|
||||
sOperationText={downloadInfo}
|
||||
/>
|
||||
)}
|
||||
{hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>}
|
||||
</ConfirmModal>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ConfirmModal } from 'decky-frontend-lib';
|
||||
import { ConfirmModal } from '@decky/ui';
|
||||
import { FC } from 'react';
|
||||
|
||||
import { uninstallPlugin } from '../../plugin';
|
||||
|
||||
interface PluginUninstallModalProps {
|
||||
name: string;
|
||||
title: string;
|
||||
@@ -14,11 +16,12 @@ const PluginUninstallModal: FC<PluginUninstallModalProps> = ({ name, title, butt
|
||||
<ConfirmModal
|
||||
closeModal={closeModal}
|
||||
onOK={async () => {
|
||||
await window.DeckyPluginLoader.callServerMethod('uninstall_plugin', { name });
|
||||
// uninstalling a plugin resets the frozen and hidden setting for it server-side
|
||||
await uninstallPlugin(name);
|
||||
// uninstalling a plugin resets the hidden setting for it server-side
|
||||
// we invalidate here so if you re-install it, you won't have an out-of-date hidden filter
|
||||
await window.DeckyPluginLoader.frozenPluginsService.invalidate();
|
||||
await window.DeckyPluginLoader.hiddenPluginsService.invalidate();
|
||||
await DeckyPluginLoader.frozenPluginsService.invalidate();
|
||||
await DeckyPluginLoader.hiddenPluginsService.invalidate();
|
||||
closeModal?.();
|
||||
}}
|
||||
strTitle={title}
|
||||
strOKButtonText={buttonText}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
SteamSpinner,
|
||||
TextField,
|
||||
ToggleField,
|
||||
} from 'decky-frontend-lib';
|
||||
} from '@decky/ui';
|
||||
import { filesize } from 'filesize';
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { DefaultExtensionType, FileIcon, defaultStyles } from 'react-file-icon';
|
||||
@@ -95,29 +95,20 @@ const sortOptions = [
|
||||
},
|
||||
];
|
||||
|
||||
function getList(
|
||||
path: string,
|
||||
includeFiles: boolean,
|
||||
includeFolders: boolean = true,
|
||||
includeExt: string[] | null = null,
|
||||
includeHidden: boolean = false,
|
||||
orderBy: SortOptions = SortOptions.name_desc,
|
||||
filterFor: RegExp | ((file: File) => boolean) | null = null,
|
||||
pageNumber: number = 1,
|
||||
max: number = 1000,
|
||||
): Promise<{ result: FileListing | string; success: boolean }> {
|
||||
return window.DeckyPluginLoader.callServerMethod('filepicker_ls', {
|
||||
path,
|
||||
include_files: includeFiles,
|
||||
include_folders: includeFolders,
|
||||
include_ext: includeExt ? includeExt : [],
|
||||
include_hidden: includeHidden,
|
||||
order_by: orderBy,
|
||||
filter_for: filterFor,
|
||||
page: pageNumber,
|
||||
max: max,
|
||||
});
|
||||
}
|
||||
const getList = DeckyBackend.callable<
|
||||
[
|
||||
path: string,
|
||||
includeFiles?: boolean,
|
||||
includeFolders?: boolean,
|
||||
includeExt?: string[] | null,
|
||||
includeHidden?: boolean,
|
||||
orderBy?: SortOptions,
|
||||
filterFor?: RegExp | ((file: File) => boolean) | null,
|
||||
pageNumber?: number,
|
||||
max?: number,
|
||||
],
|
||||
FileListing
|
||||
>('utilities/filepicker_ls');
|
||||
|
||||
const iconStyles = {
|
||||
paddingRight: '10px',
|
||||
@@ -126,20 +117,20 @@ const iconStyles = {
|
||||
|
||||
const FilePicker: FunctionComponent<FilePickerProps> = ({
|
||||
startPath,
|
||||
//What are we allowing to show in the file picker
|
||||
// What are we allowing to show in the file picker
|
||||
includeFiles = true,
|
||||
includeFolders = true,
|
||||
//Parameter for specifying a specific filename match
|
||||
// Parameter for specifying a specific filename match
|
||||
filter = undefined,
|
||||
//Filter for specific extensions as an array
|
||||
// Filter for specific extensions as an array
|
||||
validFileExtensions = undefined,
|
||||
//Allow to override the fixed extension above
|
||||
// Allow to override the fixed extension above
|
||||
allowAllFiles = true,
|
||||
//If we need to show hidden files and folders (both Win and Linux should work)
|
||||
// If we need to show hidden files and folders (both Win and Linux should work)
|
||||
defaultHidden = false, // false by default makes sense for most users
|
||||
//How much files per page to show, default 1000
|
||||
// How many files per page to show, default 1000
|
||||
max = 1000,
|
||||
//Which picking option to select by default
|
||||
// Which picking option to select by default
|
||||
fileSelType = FileSelectionType.FOLDER,
|
||||
onSubmit,
|
||||
closeModal,
|
||||
@@ -190,21 +181,27 @@ const FilePicker: FunctionComponent<FilePickerProps> = ({
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setLoading(true);
|
||||
const listing = await getList(
|
||||
path,
|
||||
includeFiles,
|
||||
includeFolders,
|
||||
selectedExts,
|
||||
showHidden,
|
||||
sort,
|
||||
filter,
|
||||
page,
|
||||
max,
|
||||
);
|
||||
if (!listing.success) {
|
||||
try {
|
||||
const listing = await getList(
|
||||
path,
|
||||
includeFiles,
|
||||
includeFolders,
|
||||
selectedExts,
|
||||
showHidden,
|
||||
sort,
|
||||
filter,
|
||||
page,
|
||||
max,
|
||||
);
|
||||
setRawError(null);
|
||||
setError(FileErrorTypes.None);
|
||||
setFiles(listing.files);
|
||||
setLoading(false);
|
||||
setListing(listing);
|
||||
logger.log('reloaded', path, listing);
|
||||
} catch (theError: any) {
|
||||
setListing({ files: [], realpath: path, total: 0 });
|
||||
setLoading(false);
|
||||
const theError = listing.result as string;
|
||||
switch (theError) {
|
||||
case theError.match(/\[Errno\s2.*/i)?.input:
|
||||
case theError.match(/\[WinError\s3.*/i)?.input:
|
||||
@@ -220,14 +217,7 @@ const FilePicker: FunctionComponent<FilePickerProps> = ({
|
||||
}
|
||||
logger.debug(theError);
|
||||
return;
|
||||
} else {
|
||||
setRawError(null);
|
||||
setError(FileErrorTypes.None);
|
||||
setFiles((listing.result as FileListing).files);
|
||||
}
|
||||
setLoading(false);
|
||||
setListing(listing.result as FileListing);
|
||||
logger.log('reloaded', path, listing);
|
||||
})();
|
||||
}, [error, path, includeFiles, includeFolders, showHidden, sort, selectedExts, page]);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Patch, findModuleChild, replacePatch, sleep } from 'decky-frontend-lib';
|
||||
import { Export, Patch, findModuleExport, replacePatch, sleep } from '@decky/ui';
|
||||
|
||||
import Logger from '../../../../logger';
|
||||
import { FileSelectionType } from '..';
|
||||
|
||||
const logger = new Logger('LibraryPatch');
|
||||
|
||||
@@ -13,8 +14,11 @@ function rePatch() {
|
||||
const details = window.appDetailsStore.GetAppDetails(appid);
|
||||
logger.debug('game details', details);
|
||||
// strShortcutStartDir
|
||||
const file = await window.DeckyPluginLoader.openFilePicker(
|
||||
const file = await DeckyPluginLoader.openFilePicker(
|
||||
FileSelectionType.FILE,
|
||||
details?.strShortcutStartDir.replaceAll('"', '') || '/',
|
||||
true,
|
||||
true,
|
||||
);
|
||||
logger.debug('user selected', file);
|
||||
window.SteamClient.Apps.SetShortcutExe(appid, JSON.stringify(file.path));
|
||||
@@ -35,12 +39,7 @@ export default async function libraryPatch() {
|
||||
let History: any;
|
||||
|
||||
while (!History) {
|
||||
History = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.m_history) return m[prop].m_history;
|
||||
}
|
||||
});
|
||||
History = findModuleExport((e: Export) => e.m_history)?.m_history;
|
||||
if (!History) {
|
||||
logger.debug('Waiting 5s for history to become available.');
|
||||
await sleep(5000);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Focusable, updaterFieldClasses } from 'decky-frontend-lib';
|
||||
import { Focusable, updaterFieldClasses } from '@decky/ui';
|
||||
import { FunctionComponent, ReactNode } from 'react';
|
||||
|
||||
interface InlinePatchNotesProps {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SidebarNavigation } from 'decky-frontend-lib';
|
||||
import { SidebarNavigation } from '@decky/ui';
|
||||
import { lazy } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaCode, FaFlask, FaPlug } from 'react-icons/fa';
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
Navigation,
|
||||
TextField,
|
||||
Toggle,
|
||||
} from 'decky-frontend-lib';
|
||||
} from '@decky/ui';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaFileArchive, FaLink, FaReact, FaSteamSymbol, FaTerminal } from 'react-icons/fa';
|
||||
@@ -28,22 +28,17 @@ const installFromZip = async () => {
|
||||
logger.error('The default path has not been found!');
|
||||
return;
|
||||
}
|
||||
window.DeckyPluginLoader.openFilePickerV2(
|
||||
FileSelectionType.FILE,
|
||||
path,
|
||||
true,
|
||||
true,
|
||||
undefined,
|
||||
['zip'],
|
||||
false,
|
||||
false,
|
||||
).then((val) => {
|
||||
const url = `file://${val.path}`;
|
||||
console.log(`Installing plugin locally from ${url}`);
|
||||
installFromURL(url);
|
||||
});
|
||||
DeckyPluginLoader.openFilePicker(FileSelectionType.FILE, path, true, true, undefined, ['zip'], false, false).then(
|
||||
(val) => {
|
||||
const url = `file://${val.path}`;
|
||||
console.log(`Installing plugin locally from ${url}`);
|
||||
installFromURL(url);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const getTabID = DeckyBackend.callable<[name: string], string>('utilities/get_tab_id');
|
||||
|
||||
export default function DeveloperSettings() {
|
||||
const [enableValveInternal, setEnableValveInternal] = useSetting<boolean>('developer.valve_internal', false);
|
||||
const [reactDevtoolsEnabled, setReactDevtoolsEnabled] = useSetting<boolean>('developer.rdt.enabled', false);
|
||||
@@ -91,13 +86,13 @@ export default function DeveloperSettings() {
|
||||
>
|
||||
<DialogButton
|
||||
onClick={async () => {
|
||||
let res = await window.DeckyPluginLoader.callServerMethod('get_tab_id', { name: 'SharedJSContext' });
|
||||
if (res.success) {
|
||||
try {
|
||||
let tabId = await getTabID('SharedJSContext');
|
||||
Navigation.NavigateToExternalWeb(
|
||||
'localhost:8080/devtools/inspector.html?ws=localhost:8080/devtools/page/' + res.result,
|
||||
'localhost:8080/devtools/inspector.html?ws=localhost:8080/devtools/page/' + tabId,
|
||||
);
|
||||
} else {
|
||||
console.error('Unable to find ID for SharedJSContext tab ', res.result);
|
||||
} catch (e) {
|
||||
console.error('Unable to find ID for SharedJSContext tab ', e);
|
||||
Navigation.NavigateToExternalWeb('localhost:8080');
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Dropdown, Field } from 'decky-frontend-lib';
|
||||
import { Dropdown, Field } from '@decky/ui';
|
||||
import { FunctionComponent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Logger from '../../../../logger';
|
||||
import { callUpdaterMethod } from '../../../../updater';
|
||||
import { checkForUpdates } from '../../../../updater';
|
||||
import { useSetting } from '../../../../utils/hooks/useSetting';
|
||||
|
||||
const logger = new Logger('BranchSelect');
|
||||
@@ -42,7 +42,7 @@ const BranchSelect: FunctionComponent<{}> = () => {
|
||||
selectedOption={selectedBranch}
|
||||
onChange={async (newVal) => {
|
||||
await setSelectedBranch(newVal.data);
|
||||
callUpdaterMethod('check_for_updates');
|
||||
checkForUpdates();
|
||||
logger.log('switching branches!');
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Field, Toggle } from 'decky-frontend-lib';
|
||||
import { Field, Toggle } from '@decky/ui';
|
||||
import { FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useDeckyState } from '../../../DeckyState';
|
||||
|
||||
const NotificationSettings: FC = () => {
|
||||
const { notificationSettings } = useDeckyState();
|
||||
const notificationService = window.DeckyPluginLoader.notificationService;
|
||||
const notificationService = DeckyPluginLoader.notificationService;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Field, Toggle } from 'decky-frontend-lib';
|
||||
import { Field, Toggle } from '@decky/ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaChrome } from 'react-icons/fa';
|
||||
|
||||
@@ -18,8 +18,8 @@ export default function RemoteDebuggingSettings() {
|
||||
value={allowRemoteDebugging || false}
|
||||
onChange={(toggleValue) => {
|
||||
setAllowRemoteDebugging(toggleValue);
|
||||
if (toggleValue) window.DeckyPluginLoader.callServerMethod('allow_remote_debugging');
|
||||
else window.DeckyPluginLoader.callServerMethod('disallow_remote_debugging');
|
||||
if (toggleValue) DeckyBackend.call('utilities/allow_remote_debugging');
|
||||
else DeckyBackend.call('utilities/disallow_remote_debugging');
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Dropdown, Field, TextField } from 'decky-frontend-lib';
|
||||
import { Dropdown, Field, TextField } from '@decky/ui';
|
||||
import { FunctionComponent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaShapes } from 'react-icons/fa';
|
||||
|
||||
@@ -8,14 +8,12 @@ import {
|
||||
Spinner,
|
||||
findSP,
|
||||
showModal,
|
||||
} from 'decky-frontend-lib';
|
||||
import { useCallback } from 'react';
|
||||
import { Suspense, lazy } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
} from '@decky/ui';
|
||||
import { Suspense, lazy, useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaExclamation } from 'react-icons/fa';
|
||||
|
||||
import { VerInfo, callUpdaterMethod, finishUpdate } from '../../../../updater';
|
||||
import { VerInfo, checkForUpdates, doUpdate } from '../../../../updater';
|
||||
import { useDeckyState } from '../../../DeckyState';
|
||||
import InlinePatchNotes from '../../../patchnotes/InlinePatchNotes';
|
||||
import WithSuspense from '../../../WithSuspense';
|
||||
@@ -68,7 +66,7 @@ function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | n
|
||||
}
|
||||
|
||||
export default function UpdaterSettings() {
|
||||
const { isLoaderUpdating, setIsLoaderUpdating, versionInfo, setVersionInfo } = useDeckyState();
|
||||
const { isLoaderUpdating, versionInfo, setVersionInfo } = useDeckyState();
|
||||
|
||||
const [checkingForUpdates, setCheckingForUpdates] = useState<boolean>(false);
|
||||
const [updateProgress, setUpdateProgress] = useState<number>(-1);
|
||||
@@ -77,16 +75,18 @@ export default function UpdaterSettings() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
window.DeckyUpdater = {
|
||||
updateProgress: (i) => {
|
||||
setUpdateProgress(i);
|
||||
setIsLoaderUpdating(true);
|
||||
},
|
||||
finish: async () => {
|
||||
setUpdateProgress(0);
|
||||
setReloading(true);
|
||||
await finishUpdate();
|
||||
},
|
||||
const a = DeckyBackend.addEventListener('updater/update_download_percentage', (percentage) => {
|
||||
setUpdateProgress(percentage);
|
||||
});
|
||||
|
||||
const b = DeckyBackend.addEventListener('updater/finish_download', () => {
|
||||
setUpdateProgress(0);
|
||||
setReloading(true);
|
||||
});
|
||||
|
||||
return () => {
|
||||
DeckyBackend.removeEventListener('updater/update_download_percentage', a);
|
||||
DeckyBackend.removeEventListener('updater/finish_download', b);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -122,13 +122,13 @@ export default function UpdaterSettings() {
|
||||
!versionInfo?.remote || versionInfo?.remote?.tag_name == versionInfo?.current
|
||||
? async () => {
|
||||
setCheckingForUpdates(true);
|
||||
const res = (await callUpdaterMethod('check_for_updates')) as { result: VerInfo };
|
||||
setVersionInfo(res.result);
|
||||
const verInfo = await checkForUpdates();
|
||||
setVersionInfo(verInfo);
|
||||
setCheckingForUpdates(false);
|
||||
}
|
||||
: async () => {
|
||||
setUpdateProgress(0);
|
||||
callUpdaterMethod('do_update');
|
||||
doUpdate();
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DialogBody, DialogControlsSection, DialogControlsSectionHeader, Field, Toggle } from 'decky-frontend-lib';
|
||||
import { DialogBody, DialogControlsSection, DialogControlsSectionHeader, Field, Toggle } from '@decky/ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useDeckyState } from '../../../DeckyState';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user