mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-13 12:15:09 +03:00
Compare commits
174 Commits
v2
..
aa/websockets
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -25,13 +25,17 @@ jobs:
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.11.4"
|
||||
|
||||
- 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
|
||||
with:
|
||||
name: PluginLoader Win
|
||||
path: |
|
||||
./dist/PluginLoader.exe
|
||||
./dist/PluginLoader_noconsole.exe
|
||||
./backend/dist/PluginLoader.exe
|
||||
./backend/dist/PluginLoader_noconsole.exe
|
||||
|
||||
|
||||
+17
-200
@@ -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,13 +13,10 @@ 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
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up NodeJS 18 💎
|
||||
uses: actions/setup-node@v3
|
||||
@@ -69,12 +45,16 @@ jobs:
|
||||
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 +67,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
|
||||
with:
|
||||
name: PluginLoader
|
||||
path: ./dist/PluginLoader
|
||||
path: ./backend/dist/PluginLoader
|
||||
|
||||
- name: Download package artifact locally
|
||||
if: ${{ env.ACT }}
|
||||
uses: actions/upload-artifact@v3
|
||||
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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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-latest
|
||||
outputs:
|
||||
tag_name: ${{ steps.ready_tag.outputs.tag_name }}
|
||||
|
||||
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: 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-latest
|
||||
needs: [create_tag, build]
|
||||
steps:
|
||||
- name: Fetch package artifact ⬇️
|
||||
uses: actions/download-artifact@v3
|
||||
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
|
||||
@@ -10,13 +10,21 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2 # Check out the repository first.
|
||||
- uses: actions/checkout@v3 # Check out the repository first.
|
||||
|
||||
- name: Install Python dependencies
|
||||
- name: Set up Python 3.10.6 🐍
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10.6"
|
||||
|
||||
- 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 +41,4 @@ jobs:
|
||||
|
||||
- name: Run tsc (TypeScript)
|
||||
working-directory: frontend
|
||||
run: $(pnpm bin)/tsc --noEmit
|
||||
run: pnpm run typecheck
|
||||
+6
-3
@@ -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
|
||||
@@ -159,6 +160,8 @@ backend/static
|
||||
.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:
|
||||
@@ -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, cast
|
||||
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, DirCreatedEvent, DirModifiedEvent, FileCreatedEvent, FileModifiedEvent
|
||||
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
|
||||
@@ -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__).parents[1].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__).parents[1].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,103 +140,74 @@ 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))
|
||||
|
||||
return web.Response(status=200)
|
||||
return web.Response(status=200)
|
||||
+12
-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")
|
||||
|
||||
@@ -168,6 +168,8 @@ def get_privileged_path() -> str:
|
||||
if path == None:
|
||||
path = get_unprivileged_path()
|
||||
|
||||
os.makedirs(path, exist_ok=True)
|
||||
|
||||
return path
|
||||
|
||||
def _parent_dir(path : str | None) -> str | None:
|
||||
@@ -187,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
|
||||
@@ -197,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
|
||||
|
||||
|
||||
+3
-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,6 +47,8 @@ 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:
|
||||
@@ -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,7 +1,7 @@
|
||||
# Change PyInstaller files permissions
|
||||
import sys
|
||||
from typing import Dict
|
||||
from .localplatform import (chmod, chown, service_stop, service_start,
|
||||
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, restart_webhelper)
|
||||
@@ -14,15 +14,15 @@ 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
|
||||
@@ -30,7 +30,8 @@ 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.cors.add(route) # pyright: ignore [reportUnknownMemberType]
|
||||
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'))])
|
||||
|
||||
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...")
|
||||
@@ -161,7 +158,8 @@ class PluginManager:
|
||||
# if first:
|
||||
if ON_LINUX and await tab.has_global_var("deckyHasLoaded", False):
|
||||
await restart_webhelper()
|
||||
await tab.evaluate_js("try{if (window.deckyHasLoaded){setTimeout(() => SteamClient.Browser.RestartJSContext(), 100)}else{window.deckyHasLoaded = true;(async()=>{try{while(!window.webpackChunksteamui){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)
|
||||
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:
|
||||
@@ -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})
|
||||
@@ -360,7 +439,7 @@ class Utilities:
|
||||
tab = await get_gamepadui_tab()
|
||||
self.rdt_script_id = None
|
||||
await close_old_tabs()
|
||||
await tab.evaluate_js("SteamClient.Browser.RestartJSContext();", False, True, False)
|
||||
await tab.evaluate_js("location.reload();", False, True, False)
|
||||
self.logger.info("React DevTools disabled")
|
||||
|
||||
async def get_user_info(self) -> Dict[str, str]:
|
||||
@@ -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 })
|
||||
@@ -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}}"
|
||||
}
|
||||
}
|
||||
|
||||
+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
+765
@@ -0,0 +1,765 @@
|
||||
# This file is automatically @generated by Poetry 1.8.2 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.2.2"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
|
||||
{file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
|
||||
]
|
||||
|
||||
[[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.3"
|
||||
description = "A very fast and expressive template engine."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"},
|
||||
{file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"},
|
||||
]
|
||||
|
||||
[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.8.0"
|
||||
description = "Node.js virtual environment builder"
|
||||
optional = false
|
||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
|
||||
files = [
|
||||
{file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
|
||||
{file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
setuptools = "*"
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "23.2"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
|
||||
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
|
||||
]
|
||||
|
||||
[[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 = "5.13.2"
|
||||
description = "PyInstaller bundles a Python application and all its dependencies into a single package."
|
||||
optional = false
|
||||
python-versions = "<3.13,>=3.7"
|
||||
files = [
|
||||
{file = "pyinstaller-5.13.2-py3-none-macosx_10_13_universal2.whl", hash = "sha256:16cbd66b59a37f4ee59373a003608d15df180a0d9eb1a29ff3bfbfae64b23d0f"},
|
||||
{file = "pyinstaller-5.13.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8f6dd0e797ae7efdd79226f78f35eb6a4981db16c13325e962a83395c0ec7420"},
|
||||
{file = "pyinstaller-5.13.2-py3-none-manylinux2014_i686.whl", hash = "sha256:65133ed89467edb2862036b35d7c5ebd381670412e1e4361215e289c786dd4e6"},
|
||||
{file = "pyinstaller-5.13.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:7d51734423685ab2a4324ab2981d9781b203dcae42839161a9ee98bfeaabdade"},
|
||||
{file = "pyinstaller-5.13.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:2c2fe9c52cb4577a3ac39626b84cf16cf30c2792f785502661286184f162ae0d"},
|
||||
{file = "pyinstaller-5.13.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c63ef6133eefe36c4b2f4daf4cfea3d6412ece2ca218f77aaf967e52a95ac9b8"},
|
||||
{file = "pyinstaller-5.13.2-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:aadafb6f213549a5906829bb252e586e2cf72a7fbdb5731810695e6516f0ab30"},
|
||||
{file = "pyinstaller-5.13.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:b2e1c7f5cceb5e9800927ddd51acf9cc78fbaa9e79e822c48b0ee52d9ce3c892"},
|
||||
{file = "pyinstaller-5.13.2-py3-none-win32.whl", hash = "sha256:421cd24f26144f19b66d3868b49ed673176765f92fa9f7914cd2158d25b6d17e"},
|
||||
{file = "pyinstaller-5.13.2-py3-none-win_amd64.whl", hash = "sha256:ddcc2b36052a70052479a9e5da1af067b4496f43686ca3cdda99f8367d0627e4"},
|
||||
{file = "pyinstaller-5.13.2-py3-none-win_arm64.whl", hash = "sha256:27cd64e7cc6b74c5b1066cbf47d75f940b71356166031deb9778a2579bb874c6"},
|
||||
{file = "pyinstaller-5.13.2.tar.gz", hash = "sha256:c8e5d3489c3a7cc5f8401c2d1f48a70e588f9967e391c3b06ddac1f685f8d5d2"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
altgraph = "*"
|
||||
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
|
||||
pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""}
|
||||
pyinstaller-hooks-contrib = ">=2021.4"
|
||||
pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""}
|
||||
setuptools = ">=42.0.0"
|
||||
|
||||
[package.extras]
|
||||
encryption = ["tinyaes (>=1.0.0)"]
|
||||
hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller-hooks-contrib"
|
||||
version = "2024.5"
|
||||
description = "Community maintained hooks for PyInstaller"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pyinstaller_hooks_contrib-2024.5-py2.py3-none-any.whl", hash = "sha256:0852249b7fb1e9394f8f22af2c22fa5294c2c0366157969f98c96df62410c4c6"},
|
||||
{file = "pyinstaller_hooks_contrib-2024.5.tar.gz", hash = "sha256:aa5dee25ea7ca317ad46fa16b5afc8dba3b0e43f2847e498930138885efd3cab"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
packaging = ">=22.0"
|
||||
setuptools = ">=42.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "pyright"
|
||||
version = "1.1.361"
|
||||
description = "Command line wrapper for pyright"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pyright-1.1.361-py3-none-any.whl", hash = "sha256:c50fc94ce92b5c958cfccbbe34142e7411d474da43d6c14a958667e35b9df7ea"},
|
||||
{file = "pyright-1.1.361.tar.gz", hash = "sha256:1d67933315666b05d230c85ea8fb97aaa2056e4092a13df87b7765bb9e8f1a8d"},
|
||||
]
|
||||
|
||||
[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 = "69.5.1"
|
||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"},
|
||||
{file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "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]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.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.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
|
||||
testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
|
||||
|
||||
[[package]]
|
||||
name = "watchdog"
|
||||
version = "2.3.1"
|
||||
description = "Filesystem events monitoring"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "watchdog-2.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1f1200d4ec53b88bf04ab636f9133cb703eb19768a39351cee649de21a33697"},
|
||||
{file = "watchdog-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:564e7739abd4bd348aeafbf71cc006b6c0ccda3160c7053c4a53b67d14091d42"},
|
||||
{file = "watchdog-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:95ad708a9454050a46f741ba5e2f3468655ea22da1114e4c40b8cbdaca572565"},
|
||||
{file = "watchdog-2.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a073c91a6ef0dda488087669586768195c3080c66866144880f03445ca23ef16"},
|
||||
{file = "watchdog-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa8b028750b43e80eea9946d01925168eeadb488dfdef1d82be4b1e28067f375"},
|
||||
{file = "watchdog-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:964fd236cd443933268ae49b59706569c8b741073dbfd7ca705492bae9d39aab"},
|
||||
{file = "watchdog-2.3.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:91fd146d723392b3e6eb1ac21f122fcce149a194a2ba0a82c5e4d0ee29cd954c"},
|
||||
{file = "watchdog-2.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:efe3252137392a471a2174d721e1037a0e6a5da7beb72a021e662b7000a9903f"},
|
||||
{file = "watchdog-2.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:85bf2263290591b7c5fa01140601b64c831be88084de41efbcba6ea289874f44"},
|
||||
{file = "watchdog-2.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8f2df370cd8e4e18499dd0bfdef476431bcc396108b97195d9448d90924e3131"},
|
||||
{file = "watchdog-2.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ea5d86d1bcf4a9d24610aa2f6f25492f441960cf04aed2bd9a97db439b643a7b"},
|
||||
{file = "watchdog-2.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6f5d0f7eac86807275eba40b577c671b306f6f335ba63a5c5a348da151aba0fc"},
|
||||
{file = "watchdog-2.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b848c71ef2b15d0ef02f69da8cc120d335cec0ed82a3fa7779e27a5a8527225"},
|
||||
{file = "watchdog-2.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0d9878be36d2b9271e3abaa6f4f051b363ff54dbbe7e7df1af3c920e4311ee43"},
|
||||
{file = "watchdog-2.3.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4cd61f98cb37143206818cb1786d2438626aa78d682a8f2ecee239055a9771d5"},
|
||||
{file = "watchdog-2.3.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3d2dbcf1acd96e7a9c9aefed201c47c8e311075105d94ce5e899f118155709fd"},
|
||||
{file = "watchdog-2.3.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:03f342a9432fe08107defbe8e405a2cb922c5d00c4c6c168c68b633c64ce6190"},
|
||||
{file = "watchdog-2.3.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7a596f9415a378d0339681efc08d2249e48975daae391d58f2e22a3673b977cf"},
|
||||
{file = "watchdog-2.3.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:0e1dd6d449267cc7d6935d7fe27ee0426af6ee16578eed93bacb1be9ff824d2d"},
|
||||
{file = "watchdog-2.3.1-py3-none-manylinux2014_i686.whl", hash = "sha256:7a1876f660e32027a1a46f8a0fa5747ad4fcf86cb451860eae61a26e102c8c79"},
|
||||
{file = "watchdog-2.3.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:2caf77ae137935c1466f8cefd4a3aec7017b6969f425d086e6a528241cba7256"},
|
||||
{file = "watchdog-2.3.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:53f3e95081280898d9e4fc51c5c69017715929e4eea1ab45801d5e903dd518ad"},
|
||||
{file = "watchdog-2.3.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:9da7acb9af7e4a272089bd2af0171d23e0d6271385c51d4d9bde91fe918c53ed"},
|
||||
{file = "watchdog-2.3.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:8a4d484e846dcd75e96b96d80d80445302621be40e293bfdf34a631cab3b33dc"},
|
||||
{file = "watchdog-2.3.1-py3-none-win32.whl", hash = "sha256:a74155398434937ac2780fd257c045954de5b11b5c52fc844e2199ce3eecf4cf"},
|
||||
{file = "watchdog-2.3.1-py3-none-win_amd64.whl", hash = "sha256:5defe4f0918a2a1a4afbe4dbb967f743ac3a93d546ea4674567806375b024adb"},
|
||||
{file = "watchdog-2.3.1-py3-none-win_ia64.whl", hash = "sha256:4109cccf214b7e3462e8403ab1e5b17b302ecce6c103eb2fc3afa534a7f27b96"},
|
||||
{file = "watchdog-2.3.1.tar.gz", hash = "sha256:d9f9ed26ed22a9d331820a8432c3680707ea8b54121ddcc9dc7d9f2ceeb36906"},
|
||||
]
|
||||
|
||||
[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 = "b87af38959be15deb2e6af33ab7cb70e502d20ebeabaae0348f816bc4ee736c6"
|
||||
@@ -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=[
|
||||
('locales', 'locales'),
|
||||
('static', '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,43 @@
|
||||
[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/static/*"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.10,<3.13"
|
||||
|
||||
aiohttp = "^3.9.5"
|
||||
aiohttp-jinja2 = "^1.5.1"
|
||||
aiohttp-cors = "^0.7.0"
|
||||
watchdog = "^2.1.7"
|
||||
certifi = "*"
|
||||
packaging = "^23.2"
|
||||
multidict = "^6.0.5"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pyinstaller = "^5.13.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": 1714763106,
|
||||
"narHash": "sha256-DrDHo74uTycfpAF+/qxZAMlP/Cpe04BVioJb6fdI0YY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e9be42459999a253a9f92559b1f5b72e1b44c13d",
|
||||
"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": 1714855626,
|
||||
"narHash": "sha256-fqvhXqJVykGHr6OHJ2eLhmNr76vKYqrEnXErLJ5eUe8=",
|
||||
"owner": "nix-community",
|
||||
"repo": "poetry2nix",
|
||||
"rev": "c8766d12a9efd0467998b887d6de6d838091f2b9",
|
||||
"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": 1714058656,
|
||||
"narHash": "sha256-Qv4RBm4LKuO4fNOfx9wl40W2rBbv5u5m+whxRYUMiaA=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "c6aaf729f34a36c445618580a9f95a48f5e4e03f",
|
||||
"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],
|
||||
}
|
||||
+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
@@ -43,6 +43,8 @@ export default defineConfig({
|
||||
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';
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ReorderableEntry,
|
||||
ReorderableList,
|
||||
showContextMenu,
|
||||
} from 'decky-frontend-lib';
|
||||
} from '@decky/ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaDownload, FaEllipsisH, FaRecycle } from 'react-icons/fa';
|
||||
@@ -44,6 +44,8 @@ type PluginTableData = PluginData & {
|
||||
isDeveloper: boolean;
|
||||
};
|
||||
|
||||
const reloadPluginBackend = DeckyBackend.callable<[pluginName: string], void>('loader/reload_plugin');
|
||||
|
||||
function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -58,27 +60,21 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
|
||||
showContextMenu(
|
||||
<Menu label={t('PluginListIndex.plugin_actions')}>
|
||||
<MenuItem
|
||||
onSelected={() => {
|
||||
onSelected={async () => {
|
||||
try {
|
||||
fetch(`http://127.0.0.1:1337/plugins/${name}/reload`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Authentication: window.deckyAuthToken,
|
||||
},
|
||||
});
|
||||
await reloadPluginBackend(name);
|
||||
} catch (err) {
|
||||
console.error('Error Reloading Plugin Backend', err);
|
||||
}
|
||||
|
||||
window.DeckyPluginLoader.importPlugin(name, version);
|
||||
DeckyPluginLoader.importPlugin(name, version);
|
||||
}}
|
||||
>
|
||||
{t('PluginListIndex.reload')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onSelected={() =>
|
||||
window.DeckyPluginLoader.uninstallPlugin(
|
||||
DeckyPluginLoader.uninstallPlugin(
|
||||
name,
|
||||
t('PluginLoader.plugin_uninstall.title', { name }),
|
||||
t('PluginLoader.plugin_uninstall.button'),
|
||||
@@ -161,12 +157,12 @@ export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
window.DeckyPluginLoader.checkPluginUpdates();
|
||||
DeckyPluginLoader.checkPluginUpdates();
|
||||
}, []);
|
||||
|
||||
const [pluginEntries, setPluginEntries] = useState<ReorderableEntry<PluginTableData>[]>([]);
|
||||
const frozenPluginsService = window.DeckyPluginLoader.frozenPluginsService;
|
||||
const hiddenPluginsService = window.DeckyPluginLoader.hiddenPluginsService;
|
||||
const hiddenPluginsService = DeckyPluginLoader.hiddenPluginsService;
|
||||
const frozenPluginsService = DeckyPluginLoader.frozenPluginsService;
|
||||
|
||||
useEffect(() => {
|
||||
setPluginEntries(
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { DialogBody, DialogButton, DialogControlsSection, Focusable, Navigation } from 'decky-frontend-lib';
|
||||
import {
|
||||
DialogBody,
|
||||
DialogButton,
|
||||
DialogControlsSection,
|
||||
Field,
|
||||
Focusable,
|
||||
Navigation,
|
||||
ProgressBar,
|
||||
SteamSpinner,
|
||||
} from '@decky/ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaDownload, FaInfo } from 'react-icons/fa';
|
||||
|
||||
import { callUpdaterMethod } from '../../../../updater';
|
||||
import { setSetting } from '../../../../utils/settings';
|
||||
import { UpdateBranch } from '../general/BranchSelect';
|
||||
|
||||
@@ -14,16 +22,47 @@ interface TestingVersion {
|
||||
head_sha: string;
|
||||
}
|
||||
|
||||
const getTestingVersions = DeckyBackend.callable<[], TestingVersion[]>('updater/get_testing_versions');
|
||||
const downloadTestingVersion = DeckyBackend.callable<[pr_id: number, sha: string]>('updater/download_testing_version');
|
||||
|
||||
export default function TestingVersionList() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [testingVersions, setTestingVersions] = useState<TestingVersion[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [updateProgress, setUpdateProgress] = useState<number | null>(null);
|
||||
const [reloading, setReloading] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setTestingVersions((await callUpdaterMethod('get_testing_versions')).result);
|
||||
setTestingVersions(await getTestingVersions());
|
||||
setLoading(false);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const a = DeckyBackend.addEventListener('updater/update_download_percentage', (percentage) => {
|
||||
setUpdateProgress(percentage);
|
||||
});
|
||||
|
||||
const b = DeckyBackend.addEventListener('updater/finish_download', () => {
|
||||
setReloading(true);
|
||||
});
|
||||
|
||||
return () => {
|
||||
DeckyBackend.removeEventListener('updater/update_download_percentage', a);
|
||||
DeckyBackend.removeEventListener('updater/finish_download', b);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<>
|
||||
<SteamSpinner>{t('Testing.loading')}</SteamSpinner>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (testingVersions.length === 0) {
|
||||
return (
|
||||
<div>
|
||||
@@ -34,49 +73,69 @@ export default function TestingVersionList() {
|
||||
|
||||
return (
|
||||
<DialogBody>
|
||||
{updateProgress !== null && <ProgressBar nProgress={updateProgress} indeterminate={reloading} />}
|
||||
<DialogControlsSection>
|
||||
<h4>{t('Testing.header')}</h4>
|
||||
<ul style={{ listStyleType: 'none', padding: '0' }}>
|
||||
{testingVersions.map((version) => {
|
||||
return (
|
||||
<li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', paddingBottom: '10px' }}>
|
||||
<span>
|
||||
{version.name} <span style={{ opacity: '50%' }}>{'#' + version.id}</span>
|
||||
</span>
|
||||
<Focusable style={{ height: '40px', marginLeft: 'auto', display: 'flex' }}>
|
||||
<DialogButton
|
||||
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
|
||||
onClick={() => {
|
||||
callUpdaterMethod('download_testing_version', { pr_id: version.id, sha_id: version.head_sha });
|
||||
setSetting('branch', UpdateBranch.Testing);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
minWidth: '150px',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
<li>
|
||||
<Field
|
||||
label={
|
||||
<>
|
||||
{version.name} <span style={{ opacity: '50%' }}>{'#' + version.id}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Focusable style={{ height: '40px', marginLeft: 'auto', display: 'flex' }}>
|
||||
<DialogButton
|
||||
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
|
||||
onClick={async () => {
|
||||
DeckyPluginLoader.toaster.toast({
|
||||
title: t('Testing.start_download_toast', { id: version.id }),
|
||||
body: null,
|
||||
});
|
||||
try {
|
||||
await downloadTestingVersion(version.id, version.head_sha);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
DeckyPluginLoader.toaster.toast({
|
||||
title: t('Testing.error'),
|
||||
body: `${e.name}: ${e.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
setSetting('branch', UpdateBranch.Testing);
|
||||
}}
|
||||
>
|
||||
{t('Testing.download')}
|
||||
<FaDownload style={{ paddingLeft: '1rem' }} />
|
||||
</div>
|
||||
</DialogButton>
|
||||
<DialogButton
|
||||
style={{
|
||||
height: '40px',
|
||||
width: '40px',
|
||||
padding: '10px 12px',
|
||||
minWidth: '40px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onClick={() => Navigation.NavigateToExternalWeb(version.link)}
|
||||
>
|
||||
<FaInfo />
|
||||
</DialogButton>
|
||||
</Focusable>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
minWidth: '150px',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{t('Testing.download')}
|
||||
<FaDownload style={{ paddingLeft: '1rem' }} />
|
||||
</div>
|
||||
</DialogButton>
|
||||
<DialogButton
|
||||
style={{
|
||||
height: '40px',
|
||||
width: '40px',
|
||||
padding: '10px 12px',
|
||||
minWidth: '40px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onClick={() => Navigation.NavigateToExternalWeb(version.link)}
|
||||
>
|
||||
<FaInfo />
|
||||
</DialogButton>
|
||||
</Focusable>
|
||||
</Field>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
ButtonItem,
|
||||
Dropdown,
|
||||
Focusable,
|
||||
PanelSectionRow,
|
||||
SingleDropdownOption,
|
||||
SuspensefulImage,
|
||||
} from 'decky-frontend-lib';
|
||||
import { ButtonItem, Dropdown, Focusable, PanelSectionRow, SingleDropdownOption, SuspensefulImage } from '@decky/ui';
|
||||
import { CSSProperties, FC, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
Tabs,
|
||||
TextField,
|
||||
findModule,
|
||||
} from 'decky-frontend-lib';
|
||||
} from '@decky/ui';
|
||||
import { Dispatch, FC, SetStateAction, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -89,7 +89,7 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }>
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const res = await getPluginList(selectedSort[0], selectedSort[1]);
|
||||
logger.log('got data!', res);
|
||||
logger.debug('got data!', res);
|
||||
setPluginList(res);
|
||||
setPluginCount(res.length);
|
||||
})();
|
||||
@@ -98,7 +98,7 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }>
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const storeRes = await getStore();
|
||||
logger.log(`store is ${storeRes}, isTesting is ${storeRes === Store.Testing}`);
|
||||
logger.debug(`store is ${storeRes}, isTesting is ${storeRes === Store.Testing}`);
|
||||
setIsTesting(storeRes === Store.Testing);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { sleep } from 'decky-frontend-lib';
|
||||
import { sleep } from '@decky/ui';
|
||||
import { FaReact } from 'react-icons/fa';
|
||||
|
||||
import Logger from './logger';
|
||||
@@ -38,19 +38,17 @@ export async function setShowValveInternal(show: boolean) {
|
||||
}
|
||||
|
||||
export async function setShouldConnectToReactDevTools(enable: boolean) {
|
||||
window.DeckyPluginLoader.toaster.toast({
|
||||
DeckyPluginLoader.toaster.toast({
|
||||
title: enable ? (
|
||||
<TranslationHelper trans_class={TranslationClass.DEVELOPER} trans_text={'enabling'} />
|
||||
<TranslationHelper transClass={TranslationClass.DEVELOPER} transText={'enabling'} />
|
||||
) : (
|
||||
<TranslationHelper trans_class={TranslationClass.DEVELOPER} trans_text={'disabling'} />
|
||||
<TranslationHelper transClass={TranslationClass.DEVELOPER} transText={'disabling'} />
|
||||
),
|
||||
body: <TranslationHelper trans_class={TranslationClass.DEVELOPER} trans_text={'5secreload'} />,
|
||||
body: <TranslationHelper transClass={TranslationClass.DEVELOPER} transText={'5secreload'} />,
|
||||
icon: <FaReact />,
|
||||
});
|
||||
await sleep(5000);
|
||||
return enable
|
||||
? window.DeckyPluginLoader.callServerMethod('enable_rdt')
|
||||
: window.DeckyPluginLoader.callServerMethod('disable_rdt');
|
||||
return enable ? DeckyBackend.call('utilities/enable_rdt') : DeckyBackend.call('utilities/disable_rdt');
|
||||
}
|
||||
|
||||
export async function startup() {
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Patch, callOriginal, findModuleExport, replacePatch } from '@decky/ui';
|
||||
|
||||
import DeckyErrorBoundary from './components/DeckyErrorBoundary';
|
||||
import Logger from './logger';
|
||||
import { getLikelyErrorSourceFromValveError } from './utils/errors';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__ERRORBOUNDARY_HOOK_INSTANCE: any;
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorBoundaryHook extends Logger {
|
||||
private errorBoundaryPatch?: Patch;
|
||||
private errorCheckPatch?: Patch;
|
||||
public doNotReportErrors: boolean = false;
|
||||
private disableReportingTimer: number = 0;
|
||||
|
||||
constructor() {
|
||||
super('ErrorBoundaryHook');
|
||||
|
||||
this.log('Initialized');
|
||||
window.__ERRORBOUNDARY_HOOK_INSTANCE?.deinit?.();
|
||||
window.__ERRORBOUNDARY_HOOK_INSTANCE = this;
|
||||
}
|
||||
|
||||
init() {
|
||||
// valve writes only the sanest of code
|
||||
const exp = /^\(\)=>\(.\|\|.\(new .\),.\)$/;
|
||||
const initErrorReportingStore = findModuleExport(
|
||||
(e) => typeof e == 'function' && e?.toString && exp.test(e.toString()),
|
||||
);
|
||||
|
||||
if (!initErrorReportingStore) {
|
||||
this.error('could not find initErrorReportingStore! error boundary hook disabled!');
|
||||
return;
|
||||
}
|
||||
// will replace the existing one for us seemingly? doesnt matter anyway lol
|
||||
const errorReportingStore = initErrorReportingStore();
|
||||
|
||||
// NUH UH.
|
||||
// Object.defineProperty(Object.getPrototypeOf(errorReportingStore), 'reporting_enabled', {
|
||||
// get: () => false,
|
||||
// });
|
||||
// errorReportingStore.m_bEnabled = false;
|
||||
|
||||
// @ts-ignore
|
||||
// window.errorStore = errorReportingStore;
|
||||
|
||||
const react15069WorkaroundRegex = / at .+\.componentDidCatch\..+\.callback /;
|
||||
this.errorCheckPatch = replacePatch(Object.getPrototypeOf(errorReportingStore), 'BIsBlacklisted', (args: any[]) => {
|
||||
const [errorSource, wasPlugin, shouldReport] = getLikelyErrorSourceFromValveError(args[0]);
|
||||
this.debug('Caught an error', args, {
|
||||
errorSource,
|
||||
wasPlugin,
|
||||
shouldReport,
|
||||
skipAllReporting: this.doNotReportErrors || this.disableReportingTimer,
|
||||
});
|
||||
if (!shouldReport) this.temporarilyDisableReporting();
|
||||
// react#15069 workaround. this took 2 hours to figure out.
|
||||
if (
|
||||
args[0]?.message?.[3]?.[0] &&
|
||||
args[0]?.message?.[1]?.[0] == ' at console.error ' &&
|
||||
react15069WorkaroundRegex.test(args[0].message[3][0])
|
||||
) {
|
||||
this.debug('ignoring early report caused by react#15069');
|
||||
return true;
|
||||
}
|
||||
if (this.doNotReportErrors || this.disableReportingTimer) return true;
|
||||
return shouldReport ? callOriginal : true;
|
||||
});
|
||||
|
||||
const ValveErrorBoundary = findModuleExport(
|
||||
(e) => e.InstallErrorReportingStore && e?.prototype?.Reset && e?.prototype?.componentDidCatch,
|
||||
);
|
||||
if (!ValveErrorBoundary) {
|
||||
this.error('could not find ValveErrorBoundary');
|
||||
return;
|
||||
}
|
||||
|
||||
this.errorBoundaryPatch = replacePatch(ValveErrorBoundary.prototype, 'render', function (this: any) {
|
||||
if (this.state.error) {
|
||||
const store = Object.getPrototypeOf(this)?.constructor?.sm_ErrorReportingStore || errorReportingStore;
|
||||
return (
|
||||
<DeckyErrorBoundary
|
||||
error={this.state.error}
|
||||
errorKey={this.props.errorKey}
|
||||
identifier={`${store.product}_${store.version}_${this.state.identifierHash}`}
|
||||
reset={() => this.Reset()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return callOriginal;
|
||||
});
|
||||
}
|
||||
|
||||
public temporarilyDisableReporting() {
|
||||
this.debug('Reporting disabled for 30s due to a non-steam error.');
|
||||
if (this.disableReportingTimer) {
|
||||
clearTimeout(this.disableReportingTimer);
|
||||
}
|
||||
this.disableReportingTimer = setTimeout(() => {
|
||||
this.debug('Reporting re-enabled after 30s timeout.');
|
||||
this.disableReportingTimer = 0;
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.errorCheckPatch?.unpatch();
|
||||
this.errorBoundaryPatch?.unpatch();
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundaryHook;
|
||||
+17
-5
@@ -1,17 +1,29 @@
|
||||
// Sets up DFL, then loads start.ts which starts up the loader
|
||||
interface Window {
|
||||
// Shut up TS
|
||||
SP_REACTDOM: any;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
// Wait for react to definitely be loaded
|
||||
while (!window.webpackChunksteamui || window.webpackChunksteamui <= 3) {
|
||||
await new Promise((r) => setTimeout(r, 10)); // Can't use DFL sleep here.
|
||||
}
|
||||
|
||||
if (!window.SP_REACT) {
|
||||
console.debug('Setting up React globals...');
|
||||
console.debug('[Decky:Boot] Setting up React globals...');
|
||||
// deliberate partial import
|
||||
const DFLWebpack = await import('decky-frontend-lib/dist/webpack');
|
||||
// TODO move these finds to dfl in v4
|
||||
const DFLWebpack = await import('@decky/ui/dist/webpack');
|
||||
window.SP_REACT = DFLWebpack.findModule((m) => m.Component && m.PureComponent && m.useLayoutEffect);
|
||||
window.SP_REACTDOM = DFLWebpack.findModule((m) => m.createPortal && m.createRoot);
|
||||
}
|
||||
console.debug('Setting up decky-frontend-lib...');
|
||||
window.DFL = await import('decky-frontend-lib');
|
||||
console.debug('[Decky:Boot] Setting up @decky/ui...');
|
||||
window.DFL = await import('@decky/ui');
|
||||
console.debug('[Decky:Boot] Authenticating with Decky backend...');
|
||||
window.deckyAuthToken = await fetch('http://127.0.0.1:1337/auth/token').then((r) => r.text());
|
||||
console.debug('[Decky:Boot] Connecting to Decky backend...');
|
||||
window.DeckyBackend = new (await import('./wsrouter')).WSRouter();
|
||||
await DeckyBackend.connect();
|
||||
console.debug('[Decky:Boot] Starting Decky!');
|
||||
await import('./start');
|
||||
})();
|
||||
|
||||
@@ -18,6 +18,16 @@ export const debug = (name: string, ...args: any[]) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const warn = (name: string, ...args: any[]) => {
|
||||
console.warn(
|
||||
`%c Decky %c ${name} %c`,
|
||||
'background: #16a085; color: black;',
|
||||
'background: #ffbb00; color: black;',
|
||||
'color: blue;',
|
||||
...args,
|
||||
);
|
||||
};
|
||||
|
||||
export const error = (name: string, ...args: any[]) => {
|
||||
console.error(
|
||||
`%c Decky %c ${name} %c`,
|
||||
@@ -41,6 +51,10 @@ class Logger {
|
||||
debug(this.name, ...args);
|
||||
}
|
||||
|
||||
warn(...args: any[]) {
|
||||
warn(this.name, ...args);
|
||||
}
|
||||
|
||||
error(...args: any[]) {
|
||||
error(this.name, ...args);
|
||||
}
|
||||
|
||||
+356
-180
@@ -2,18 +2,17 @@ import {
|
||||
ModalRoot,
|
||||
PanelSection,
|
||||
PanelSectionRow,
|
||||
Patch,
|
||||
QuickAccessTab,
|
||||
Router,
|
||||
findSP,
|
||||
quickAccessMenuClasses,
|
||||
showModal,
|
||||
sleep,
|
||||
} from 'decky-frontend-lib';
|
||||
} from '@decky/ui';
|
||||
import { FC, lazy } from 'react';
|
||||
import { FaExclamationCircle, FaPlug } from 'react-icons/fa';
|
||||
|
||||
import { DeckyState, DeckyStateContextProvider, UserInfo, useDeckyState } from './components/DeckyState';
|
||||
import LegacyPlugin from './components/LegacyPlugin';
|
||||
import { File, FileSelectionType } from './components/modals/filepicker';
|
||||
import { deinitFilepickerPatches, initFilepickerPatches } from './components/modals/filepicker/patches';
|
||||
import MultiplePluginsInstallModal from './components/modals/MultiplePluginsInstallModal';
|
||||
@@ -22,18 +21,18 @@ import PluginUninstallModal from './components/modals/PluginUninstallModal';
|
||||
import NotificationBadge from './components/NotificationBadge';
|
||||
import PluginView from './components/PluginView';
|
||||
import WithSuspense from './components/WithSuspense';
|
||||
import ErrorBoundaryHook from './errorboundary-hook';
|
||||
import { FrozenPluginService } from './frozen-plugins-service';
|
||||
import { HiddenPluginsService } from './hidden-plugins-service';
|
||||
import Logger from './logger';
|
||||
import { NotificationService } from './notification-service';
|
||||
import { InstallType, Plugin } from './plugin';
|
||||
import { InstallType, Plugin, PluginLoadType } from './plugin';
|
||||
import RouterHook from './router-hook';
|
||||
import { deinitSteamFixes, initSteamFixes } from './steamfixes';
|
||||
import { checkForUpdates } from './store';
|
||||
import { checkForPluginUpdates } from './store';
|
||||
import TabsHook from './tabs-hook';
|
||||
import OldTabsHook from './tabs-hook.old';
|
||||
import Toaster from './toaster';
|
||||
import { VerInfo, callUpdaterMethod } from './updater';
|
||||
import { getVersionInfo } from './updater';
|
||||
import { getSetting, setSetting } from './utils/settings';
|
||||
import TranslationHelper, { TranslationClass } from './utils/TranslationHelper';
|
||||
|
||||
@@ -42,13 +41,34 @@ const SettingsPage = lazy(() => import('./components/settings'));
|
||||
|
||||
const FilePicker = lazy(() => import('./components/modals/filepicker'));
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit?: {
|
||||
connect: (version: number, key: string) => any; // Returns the backend API used above, no real point adding types to this.
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** Map of event names to event listeners */
|
||||
type listenerMap = Map<string, Set<(...args: any) => any>>;
|
||||
|
||||
interface DeckyRequestInit extends RequestInit {
|
||||
excludedHeaders: string[];
|
||||
}
|
||||
|
||||
const callPluginMethod = DeckyBackend.callable<[pluginName: string, method: string, ...args: any], any>(
|
||||
'loader/call_plugin_method',
|
||||
);
|
||||
|
||||
class PluginLoader extends Logger {
|
||||
private plugins: Plugin[] = [];
|
||||
private tabsHook: TabsHook | OldTabsHook = document.title == 'SP' ? new OldTabsHook() : new TabsHook();
|
||||
// private windowHook: WindowHook = new WindowHook();
|
||||
private routerHook: RouterHook = new RouterHook();
|
||||
public errorBoundaryHook: ErrorBoundaryHook = new ErrorBoundaryHook();
|
||||
private tabsHook: TabsHook = new TabsHook();
|
||||
public routerHook: RouterHook = new RouterHook();
|
||||
public toaster: Toaster = new Toaster();
|
||||
private deckyState: DeckyState = new DeckyState();
|
||||
// stores a map of plugin names to all their event listeners
|
||||
private pluginEventListeners: Map<string, listenerMap> = new Map();
|
||||
|
||||
public frozenPluginsService = new FrozenPluginService(this.deckyState);
|
||||
public hiddenPluginsService = new HiddenPluginsService(this.deckyState);
|
||||
@@ -58,12 +78,25 @@ class PluginLoader extends Logger {
|
||||
// stores a list of plugin names which requested to be reloaded
|
||||
private pluginReloadQueue: { name: string; version?: string }[] = [];
|
||||
|
||||
private focusWorkaroundPatch?: Patch;
|
||||
|
||||
constructor() {
|
||||
super(PluginLoader.name);
|
||||
|
||||
this.errorBoundaryHook.init();
|
||||
|
||||
DeckyBackend.addEventListener('loader/notify_updates', this.notifyUpdates.bind(this));
|
||||
DeckyBackend.addEventListener('loader/import_plugin', this.importPlugin.bind(this));
|
||||
DeckyBackend.addEventListener('loader/unload_plugin', this.unloadPlugin.bind(this));
|
||||
DeckyBackend.addEventListener('loader/add_plugin_install_prompt', this.addPluginInstallPrompt.bind(this));
|
||||
DeckyBackend.addEventListener(
|
||||
'loader/add_multiple_plugins_install_prompt',
|
||||
this.addMultiplePluginsInstallPrompt.bind(this),
|
||||
);
|
||||
DeckyBackend.addEventListener('updater/update_download_percentage', () => {
|
||||
this.deckyState.setIsLoaderUpdating(true);
|
||||
});
|
||||
DeckyBackend.addEventListener(`loader/plugin_event`, this.pluginEventListener);
|
||||
|
||||
this.tabsHook.init();
|
||||
this.log('Initialized');
|
||||
|
||||
const TabBadge = () => {
|
||||
const { updates, hasLoaderUpdate } = useDeckyState();
|
||||
@@ -105,19 +138,46 @@ class PluginLoader extends Logger {
|
||||
|
||||
initFilepickerPatches();
|
||||
|
||||
this.getUserInfo();
|
||||
this.initPluginBackendAPI();
|
||||
|
||||
this.updateVersion();
|
||||
Promise.all([this.getUserInfo(), this.updateVersion()])
|
||||
.then(() => this.loadPlugins())
|
||||
.then(() => this.checkPluginUpdates())
|
||||
.then(() => this.log('Initialized'));
|
||||
}
|
||||
|
||||
private getPluginsFromBackend = DeckyBackend.callable<
|
||||
[],
|
||||
{ name: string; version: string; load_type: PluginLoadType }[]
|
||||
>('loader/get_plugins');
|
||||
|
||||
private async loadPlugins() {
|
||||
// wait for SP window to exist before loading plugins
|
||||
while (!findSP()) {
|
||||
await sleep(100);
|
||||
}
|
||||
const plugins = await this.getPluginsFromBackend();
|
||||
const pluginLoadPromises = [];
|
||||
const loadStart = performance.now();
|
||||
for (const plugin of plugins) {
|
||||
if (!this.hasPlugin(plugin.name))
|
||||
pluginLoadPromises.push(this.importPlugin(plugin.name, plugin.version, plugin.load_type, false));
|
||||
}
|
||||
await Promise.all(pluginLoadPromises);
|
||||
const loadEnd = performance.now();
|
||||
this.log(`Loaded ${plugins.length} plugins in ${loadEnd - loadStart}ms`);
|
||||
|
||||
this.checkPluginUpdates();
|
||||
}
|
||||
|
||||
public async getUserInfo() {
|
||||
const userInfo = (await this.callServerMethod('get_user_info')).result as UserInfo;
|
||||
const userInfo = await DeckyBackend.call<[], UserInfo>('utilities/get_user_info');
|
||||
setSetting('user_info.user_name', userInfo.username);
|
||||
setSetting('user_info.user_home', userInfo.path);
|
||||
}
|
||||
|
||||
public async updateVersion() {
|
||||
const versionInfo = (await callUpdaterMethod('get_version')).result as VerInfo;
|
||||
const versionInfo = await getVersionInfo();
|
||||
this.deckyState.setVersionInfo(versionInfo);
|
||||
|
||||
return versionInfo;
|
||||
@@ -129,12 +189,12 @@ class PluginLoader extends Logger {
|
||||
this.deckyState.setHasLoaderUpdate(true);
|
||||
if (this.notificationService.shouldNotify('deckyUpdates')) {
|
||||
this.toaster.toast({
|
||||
title: <TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="decky_title" />,
|
||||
title: <TranslationHelper transClass={TranslationClass.PLUGIN_LOADER} transText="decky_title" />,
|
||||
body: (
|
||||
<TranslationHelper
|
||||
trans_class={TranslationClass.PLUGIN_LOADER}
|
||||
trans_text="decky_update_available"
|
||||
i18n_args={{ tag_name: versionInfo?.remote?.tag_name }}
|
||||
transClass={TranslationClass.PLUGIN_LOADER}
|
||||
transText="decky_update_available"
|
||||
i18nArgs={{ tag_name: versionInfo?.remote?.tag_name }}
|
||||
/>
|
||||
),
|
||||
onClick: () => Router.Navigate('/decky/settings'),
|
||||
@@ -148,7 +208,7 @@ class PluginLoader extends Logger {
|
||||
public async checkPluginUpdates() {
|
||||
const frozenPlugins = this.deckyState.publicState().frozenPlugins;
|
||||
|
||||
const updates = await checkForUpdates(this.plugins.filter((p) => !frozenPlugins.includes(p.name)));
|
||||
const updates = await checkForPluginUpdates(this.plugins.filter((p) => !frozenPlugins.includes(p.name)));
|
||||
this.deckyState.setUpdates(updates);
|
||||
return updates;
|
||||
}
|
||||
@@ -157,12 +217,12 @@ class PluginLoader extends Logger {
|
||||
const updates = await this.checkPluginUpdates();
|
||||
if (updates?.size > 0 && this.notificationService.shouldNotify('pluginUpdates')) {
|
||||
this.toaster.toast({
|
||||
title: <TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="decky_title" />,
|
||||
title: <TranslationHelper transClass={TranslationClass.PLUGIN_LOADER} transText="decky_title" />,
|
||||
body: (
|
||||
<TranslationHelper
|
||||
trans_class={TranslationClass.PLUGIN_LOADER}
|
||||
trans_text="plugin_update"
|
||||
i18n_args={{ count: updates.size }}
|
||||
transClass={TranslationClass.PLUGIN_LOADER}
|
||||
transText="plugin_update"
|
||||
i18nArgs={{ count: updates.size }}
|
||||
/>
|
||||
),
|
||||
onClick: () => Router.Navigate('/decky/settings/plugins'),
|
||||
@@ -183,8 +243,8 @@ class PluginLoader extends Logger {
|
||||
version={version}
|
||||
hash={hash}
|
||||
installType={install_type}
|
||||
onOK={() => this.callServerMethod('confirm_plugin_install', { request_id })}
|
||||
onCancel={() => this.callServerMethod('cancel_plugin_install', { request_id })}
|
||||
onOK={() => DeckyBackend.call<[string]>('utilities/confirm_plugin_install', request_id)}
|
||||
onCancel={() => DeckyBackend.call<[string]>('utilities/cancel_plugin_install', request_id)}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
@@ -196,8 +256,8 @@ class PluginLoader extends Logger {
|
||||
showModal(
|
||||
<MultiplePluginsInstallModal
|
||||
requests={requests}
|
||||
onOK={() => this.callServerMethod('confirm_plugin_install', { request_id })}
|
||||
onCancel={() => this.callServerMethod('cancel_plugin_install', { request_id })}
|
||||
onOK={() => DeckyBackend.call<[string]>('utilities/confirm_plugin_install', request_id)}
|
||||
onCancel={() => DeckyBackend.call<[string]>('utilities/cancel_plugin_install', request_id)}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
@@ -222,9 +282,9 @@ class PluginLoader extends Logger {
|
||||
if (val) import('./developer').then((developer) => developer.startup());
|
||||
});
|
||||
|
||||
//* Grab and set plugin order
|
||||
// Grab and set plugin order
|
||||
getSetting<string[]>('pluginOrder', []).then((pluginOrder) => {
|
||||
console.log(pluginOrder);
|
||||
this.debug('pluginOrder: ', pluginOrder);
|
||||
this.deckyState.setPluginOrder(pluginOrder);
|
||||
});
|
||||
|
||||
@@ -238,151 +298,166 @@ class PluginLoader extends Logger {
|
||||
this.routerHook.removeRoute('/decky/settings');
|
||||
deinitSteamFixes();
|
||||
deinitFilepickerPatches();
|
||||
this.focusWorkaroundPatch?.unpatch();
|
||||
this.routerHook.deinit();
|
||||
this.tabsHook.deinit();
|
||||
this.toaster.deinit();
|
||||
this.errorBoundaryHook.deinit();
|
||||
}
|
||||
|
||||
public unloadPlugin(name: string) {
|
||||
console.log('Plugin List: ', this.plugins);
|
||||
const plugin = this.plugins.find((plugin) => plugin.name === name || plugin.name === name.replace('$LEGACY_', ''));
|
||||
const plugin = this.plugins.find((plugin) => plugin.name === name);
|
||||
plugin?.onDismount?.();
|
||||
this.plugins = this.plugins.filter((p) => p !== plugin);
|
||||
this.deckyState.setPlugins(this.plugins);
|
||||
}
|
||||
|
||||
public async importPlugin(name: string, version?: string | undefined) {
|
||||
if (this.reloadLock) {
|
||||
public async importPlugin(
|
||||
name: string,
|
||||
version?: string | undefined,
|
||||
loadType: PluginLoadType = PluginLoadType.ESMODULE_V1,
|
||||
useQueue: boolean = true,
|
||||
) {
|
||||
if (useQueue && this.reloadLock) {
|
||||
this.log('Reload currently in progress, adding to queue', name);
|
||||
this.pluginReloadQueue.push({ name, version: version });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.reloadLock = true;
|
||||
if (useQueue) this.reloadLock = true;
|
||||
this.log(`Trying to load ${name}`);
|
||||
|
||||
this.unloadPlugin(name);
|
||||
|
||||
if (name.startsWith('$LEGACY_')) {
|
||||
await this.importLegacyPlugin(name.replace('$LEGACY_', ''));
|
||||
} else {
|
||||
await this.importReactPlugin(name, version);
|
||||
}
|
||||
const startTime = performance.now();
|
||||
await this.importReactPlugin(name, version, loadType);
|
||||
const endTime = performance.now();
|
||||
|
||||
this.deckyState.setPlugins(this.plugins);
|
||||
this.log(`Loaded ${name}`);
|
||||
this.log(`Loaded ${name} in ${endTime - startTime}ms`);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
} finally {
|
||||
this.reloadLock = false;
|
||||
const nextPlugin = this.pluginReloadQueue.shift();
|
||||
if (nextPlugin) {
|
||||
this.importPlugin(nextPlugin.name, nextPlugin.version);
|
||||
if (useQueue) {
|
||||
this.reloadLock = false;
|
||||
const nextPlugin = this.pluginReloadQueue.shift();
|
||||
if (nextPlugin) {
|
||||
this.importPlugin(nextPlugin.name, nextPlugin.version);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async importReactPlugin(name: string, version?: string) {
|
||||
let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Authentication: window.deckyAuthToken,
|
||||
},
|
||||
});
|
||||
private async importReactPlugin(
|
||||
name: string,
|
||||
version?: string,
|
||||
loadType: PluginLoadType = PluginLoadType.ESMODULE_V1,
|
||||
) {
|
||||
try {
|
||||
switch (loadType) {
|
||||
case PluginLoadType.ESMODULE_V1:
|
||||
const plugin_exports = await import(`http://127.0.0.1:1337/plugins/${name}/dist/index.js`);
|
||||
let plugin = plugin_exports.default();
|
||||
|
||||
if (res.ok) {
|
||||
try {
|
||||
let plugin_export = await eval(await res.text());
|
||||
let plugin = plugin_export(this.createPluginAPI(name));
|
||||
this.plugins.push({
|
||||
...plugin,
|
||||
name: name,
|
||||
version: version,
|
||||
});
|
||||
} catch (e) {
|
||||
this.error('Error loading plugin ' + name, e);
|
||||
const TheError: FC<{}> = () => (
|
||||
<PanelSection>
|
||||
<PanelSectionRow>
|
||||
<div
|
||||
className={quickAccessMenuClasses.FriendsTitle}
|
||||
style={{ display: 'flex', justifyContent: 'center' }}
|
||||
>
|
||||
<TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="error" />
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
<PanelSectionRow>
|
||||
<pre style={{ overflowX: 'scroll' }}>
|
||||
<code>{e instanceof Error ? e.stack : JSON.stringify(e)}</code>
|
||||
</pre>
|
||||
</PanelSectionRow>
|
||||
<PanelSectionRow>
|
||||
<div className={quickAccessMenuClasses.Text}>
|
||||
<TranslationHelper
|
||||
trans_class={TranslationClass.PLUGIN_LOADER}
|
||||
trans_text="plugin_error_uninstall"
|
||||
i18n_args={{ name: name }}
|
||||
/>
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
</PanelSection>
|
||||
);
|
||||
this.plugins.push({
|
||||
name: name,
|
||||
version: version,
|
||||
content: <TheError />,
|
||||
icon: <FaExclamationCircle />,
|
||||
});
|
||||
this.toaster.toast({
|
||||
title: (
|
||||
<TranslationHelper
|
||||
trans_class={TranslationClass.PLUGIN_LOADER}
|
||||
trans_text="plugin_load_error.toast"
|
||||
i18n_args={{ name: name }}
|
||||
/>
|
||||
),
|
||||
body: '' + e,
|
||||
icon: <FaExclamationCircle />,
|
||||
});
|
||||
this.plugins.push({
|
||||
...plugin,
|
||||
name: name,
|
||||
version: version,
|
||||
});
|
||||
break;
|
||||
|
||||
case PluginLoadType.LEGACY_EVAL_IIFE:
|
||||
let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'X-Decky-Auth': deckyAuthToken,
|
||||
},
|
||||
});
|
||||
if (res.ok) {
|
||||
let plugin_export: (serverAPI: any) => Plugin = await eval(
|
||||
(await res.text()) + `\n//# sourceURL=decky://decky/legacy_plugin/${encodeURIComponent(name)}/index.js`,
|
||||
);
|
||||
let plugin = plugin_export(this.createLegacyPluginAPI(name));
|
||||
this.plugins.push({
|
||||
...plugin,
|
||||
name: name,
|
||||
version: version,
|
||||
});
|
||||
} else throw new Error(`${name} frontend_bundle not OK`);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`${name} has no defined loadType.`);
|
||||
}
|
||||
} else throw new Error(`${name} frontend_bundle not OK`);
|
||||
}
|
||||
|
||||
private async importLegacyPlugin(name: string) {
|
||||
const url = `http://127.0.0.1:1337/plugins/load_main/${name}`;
|
||||
this.plugins.push({
|
||||
name: name,
|
||||
icon: <FaPlug />,
|
||||
content: <LegacyPlugin url={url} />,
|
||||
});
|
||||
} catch (e) {
|
||||
this.error('Error loading plugin ' + name, e);
|
||||
const TheError: FC<{}> = () => (
|
||||
<PanelSection>
|
||||
<PanelSectionRow>
|
||||
<div className={quickAccessMenuClasses.FriendsTitle} style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<TranslationHelper transClass={TranslationClass.PLUGIN_LOADER} transText="error" />
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
<PanelSectionRow>
|
||||
<pre style={{ overflowX: 'scroll' }}>
|
||||
<code>{e instanceof Error ? e.stack : JSON.stringify(e)}</code>
|
||||
</pre>
|
||||
</PanelSectionRow>
|
||||
<PanelSectionRow>
|
||||
<div className={quickAccessMenuClasses.Text}>
|
||||
<TranslationHelper
|
||||
transClass={TranslationClass.PLUGIN_LOADER}
|
||||
transText="plugin_error_uninstall"
|
||||
i18nArgs={{ name: name }}
|
||||
/>
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
</PanelSection>
|
||||
);
|
||||
this.plugins.push({
|
||||
name: name,
|
||||
version: version,
|
||||
content: <TheError />,
|
||||
icon: <FaExclamationCircle />,
|
||||
});
|
||||
this.toaster.toast({
|
||||
title: (
|
||||
<TranslationHelper
|
||||
transClass={TranslationClass.PLUGIN_LOADER}
|
||||
transText="plugin_load_error.toast"
|
||||
i18nArgs={{ name: name }}
|
||||
/>
|
||||
),
|
||||
body: '' + e,
|
||||
icon: <FaExclamationCircle />,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async callServerMethod(methodName: string, args = {}) {
|
||||
const response = await fetch(`http://127.0.0.1:1337/methods/${methodName}`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authentication: window.deckyAuthToken,
|
||||
},
|
||||
body: JSON.stringify(args),
|
||||
});
|
||||
|
||||
return response.json();
|
||||
this.warn(
|
||||
`Calling ${methodName} via callServerMethod, which is deprecated and will be removed in a future release. Please switch to the backend API.`,
|
||||
);
|
||||
return await DeckyBackend.call<[methodName: string, kwargs: any], any>(
|
||||
'utilities/_call_legacy_utility',
|
||||
methodName,
|
||||
args,
|
||||
);
|
||||
}
|
||||
|
||||
openFilePicker(
|
||||
openFilePickerLegacy(
|
||||
startPath: string,
|
||||
selectFiles?: boolean,
|
||||
regex?: RegExp,
|
||||
): Promise<{ path: string; realpath: string }> {
|
||||
this.warn('openFilePicker is deprecated and will be removed. Please migrate to openFilePickerV2');
|
||||
if (selectFiles) {
|
||||
return this.openFilePickerV2(FileSelectionType.FILE, startPath, true, true, regex);
|
||||
return this.openFilePicker(FileSelectionType.FILE, startPath, true, true, regex);
|
||||
} else {
|
||||
return this.openFilePickerV2(FileSelectionType.FOLDER, startPath, false, true, regex);
|
||||
return this.openFilePicker(FileSelectionType.FOLDER, startPath, false, true, regex);
|
||||
}
|
||||
}
|
||||
|
||||
openFilePickerV2(
|
||||
openFilePicker(
|
||||
select: FileSelectionType,
|
||||
startPath: string,
|
||||
includeFiles?: boolean,
|
||||
@@ -423,55 +498,156 @@ class PluginLoader extends Logger {
|
||||
});
|
||||
}
|
||||
|
||||
createPluginAPI(pluginName: string) {
|
||||
return {
|
||||
routerHook: this.routerHook,
|
||||
toaster: this.toaster,
|
||||
callServerMethod: this.callServerMethod,
|
||||
openFilePicker: this.openFilePicker,
|
||||
openFilePickerV2: this.openFilePickerV2,
|
||||
async callPluginMethod(methodName: string, args = {}) {
|
||||
const response = await fetch(`http://127.0.0.1:1337/plugins/${pluginName}/methods/${methodName}`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authentication: window.deckyAuthToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
args,
|
||||
}),
|
||||
});
|
||||
// Useful for audio/video streams
|
||||
getExternalResourceURL(url: string) {
|
||||
return `http://127.0.0.1:1337/fetch?auth=${deckyAuthToken}&fetch_url=${encodeURIComponent(url)}`;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
fetchNoCors(url: string, request: any = {}) {
|
||||
let args = { method: 'POST', headers: {} };
|
||||
const req = { ...args, ...request, url, data: request.body };
|
||||
req?.body && delete req.body;
|
||||
return this.callServerMethod('http_request', req);
|
||||
},
|
||||
executeInTab(tab: string, runAsync: boolean, code: string) {
|
||||
return this.callServerMethod('execute_in_tab', {
|
||||
tab,
|
||||
run_async: runAsync,
|
||||
code,
|
||||
});
|
||||
},
|
||||
injectCssIntoTab(tab: string, style: string) {
|
||||
return this.callServerMethod('inject_css_into_tab', {
|
||||
tab,
|
||||
style,
|
||||
});
|
||||
},
|
||||
removeCssFromTab(tab: string, cssId: any) {
|
||||
return this.callServerMethod('remove_css_from_tab', {
|
||||
tab,
|
||||
css_id: cssId,
|
||||
});
|
||||
// Same syntax as fetch but only supports the url-based syntax and an object for headers since it's the most common usage pattern
|
||||
fetchNoCors(input: string, init?: DeckyRequestInit | undefined): Promise<Response> {
|
||||
const headers: { [name: string]: string } = {
|
||||
...(init?.headers as { [name: string]: string }),
|
||||
'X-Decky-Auth': deckyAuthToken,
|
||||
'X-Decky-Fetch-URL': input,
|
||||
};
|
||||
|
||||
if (init?.excludedHeaders) {
|
||||
headers['X-Decky-Fetch-Excluded-Headers'] = init.excludedHeaders.join(', ');
|
||||
}
|
||||
|
||||
return fetch('http://127.0.0.1:1337/fetch', {
|
||||
...init,
|
||||
credentials: 'include',
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
async legacyFetchNoCors(url: string, request: any = {}) {
|
||||
let method: string;
|
||||
const req = { headers: {}, ...request, data: request.body };
|
||||
req?.body && delete req.body;
|
||||
if (!request.method) {
|
||||
method = 'POST';
|
||||
} else {
|
||||
method = request.method;
|
||||
delete req.method;
|
||||
}
|
||||
// this is terrible but a. we're going to redo this entire method anyway and b. it was already terrible
|
||||
try {
|
||||
const ret = await DeckyBackend.call<
|
||||
[method: string, url: string, extra_opts?: any],
|
||||
{ status: number; headers: { [key: string]: string }; body: string }
|
||||
>('utilities/http_request', method, url, req);
|
||||
return { success: true, result: ret };
|
||||
} catch (e) {
|
||||
return { success: false, result: e?.toString() };
|
||||
}
|
||||
}
|
||||
|
||||
initPluginBackendAPI() {
|
||||
// Things will break *very* badly if plugin code touches this outside of @decky/backend, so lets make that clear.
|
||||
window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit = {
|
||||
connect: (version: number, pluginName: string) => {
|
||||
if (version < 1 || version > 1) {
|
||||
throw new Error(`Plugin ${pluginName} requested unsupported backend api version ${version}.`);
|
||||
}
|
||||
|
||||
const eventListeners: listenerMap = new Map();
|
||||
this.pluginEventListeners.set(pluginName, eventListeners);
|
||||
|
||||
const backendAPI = {
|
||||
call: (methodName: string, ...args: any) => {
|
||||
return callPluginMethod(pluginName, methodName, ...args);
|
||||
},
|
||||
callable: (methodName: string) => {
|
||||
return (...args: any) => callPluginMethod(pluginName, methodName, ...args);
|
||||
},
|
||||
addEventListener: (event: string, listener: (...args: any) => any) => {
|
||||
if (!eventListeners.has(event)) {
|
||||
eventListeners.set(event, new Set([listener]));
|
||||
} else {
|
||||
eventListeners.get(event)?.add(listener);
|
||||
}
|
||||
return listener;
|
||||
},
|
||||
removeEventListener: (event: string, listener: (...args: any) => any) => {
|
||||
if (eventListeners.has(event)) {
|
||||
const set = eventListeners.get(event);
|
||||
set?.delete(listener);
|
||||
}
|
||||
},
|
||||
openFilePicker: this.openFilePicker.bind(this),
|
||||
executeInTab: DeckyBackend.callable<
|
||||
[tab: String, runAsync: Boolean, code: string],
|
||||
{ success: boolean; result: any }
|
||||
>('utilities/execute_in_tab'),
|
||||
fetchNoCors: this.fetchNoCors.bind(this),
|
||||
getExternalResourceURL: this.getExternalResourceURL.bind(this),
|
||||
injectCssIntoTab: DeckyBackend.callable<[tab: string, style: string], string>(
|
||||
'utilities/inject_css_into_tab',
|
||||
),
|
||||
removeCssFromTab: DeckyBackend.callable<[tab: string, cssId: string]>('utilities/remove_css_from_tab'),
|
||||
routerHook: this.routerHook,
|
||||
toaster: this.toaster,
|
||||
};
|
||||
|
||||
this.debug(`${pluginName} connected to loader API.`);
|
||||
return backendAPI;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pluginEventListener = (data: { plugin: string; event: string; args: any }) => {
|
||||
const { plugin, event, args } = data;
|
||||
this.debug(`Recieved plugin event ${event} for ${plugin} with args`, args);
|
||||
if (!this.pluginEventListeners.has(plugin)) {
|
||||
this.warn(`plugin ${plugin} does not have event listeners`);
|
||||
return;
|
||||
}
|
||||
const eventListeners = this.pluginEventListeners.get(plugin)!;
|
||||
if (eventListeners.has(event)) {
|
||||
for (const listener of eventListeners.get(event)!) {
|
||||
(async () => {
|
||||
try {
|
||||
await listener(...args);
|
||||
} catch (e) {
|
||||
this.error(`error in event ${event}`, e, listener);
|
||||
}
|
||||
})();
|
||||
}
|
||||
} else {
|
||||
this.warn(`event ${event} has no listeners`);
|
||||
}
|
||||
};
|
||||
|
||||
createLegacyPluginAPI(pluginName: string) {
|
||||
const pluginAPI = {
|
||||
routerHook: this.routerHook,
|
||||
toaster: this.toaster,
|
||||
// Legacy
|
||||
callServerMethod: this.callServerMethod,
|
||||
openFilePicker: this.openFilePickerLegacy,
|
||||
openFilePickerV2: this.openFilePicker,
|
||||
// Legacy
|
||||
async callPluginMethod(methodName: string, args = {}) {
|
||||
return DeckyBackend.call<[pluginName: string, methodName: string, kwargs: any], any>(
|
||||
'loader/call_legacy_plugin_method',
|
||||
pluginName,
|
||||
methodName,
|
||||
args,
|
||||
);
|
||||
},
|
||||
fetchNoCors: this.legacyFetchNoCors,
|
||||
executeInTab: DeckyBackend.callable<
|
||||
[tab: String, runAsync: Boolean, code: string],
|
||||
{ success: boolean; result: any }
|
||||
>('utilities/execute_in_tab'),
|
||||
injectCssIntoTab: DeckyBackend.callable<[tab: string, style: string], string>('utilities/inject_css_into_tab'),
|
||||
removeCssFromTab: DeckyBackend.callable<[tab: string, cssId: string]>('utilities/remove_css_from_tab'),
|
||||
};
|
||||
|
||||
return pluginAPI;
|
||||
}
|
||||
}
|
||||
|
||||
export default PluginLoader;
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
export enum PluginLoadType {
|
||||
LEGACY_EVAL_IIFE = 0, // legacy, uses legacy serverAPI
|
||||
ESMODULE_V1 = 1, // esmodule loading with modern @decky/backend apis
|
||||
}
|
||||
|
||||
export interface Plugin {
|
||||
name: string;
|
||||
version?: string;
|
||||
@@ -13,3 +18,27 @@ export enum InstallType {
|
||||
REINSTALL,
|
||||
UPDATE,
|
||||
}
|
||||
|
||||
type installPluginArgs = [
|
||||
artifact: string,
|
||||
name?: string,
|
||||
version?: string,
|
||||
hash?: string | boolean,
|
||||
installType?: InstallType,
|
||||
];
|
||||
|
||||
export let installPlugin = DeckyBackend.callable<installPluginArgs>('utilities/install_plugin');
|
||||
|
||||
type installPluginsArgs = [
|
||||
requests: {
|
||||
artifact: string;
|
||||
name?: string;
|
||||
version?: string;
|
||||
hash?: string | boolean;
|
||||
installType?: InstallType;
|
||||
}[],
|
||||
];
|
||||
|
||||
export let installPlugins = DeckyBackend.callable<installPluginsArgs>('utilities/install_plugins');
|
||||
|
||||
export let uninstallPlugin = DeckyBackend.callable<[name: string]>('utilities/uninstall_plugin');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Patch, afterPatch, findModuleChild } from 'decky-frontend-lib';
|
||||
import { ErrorBoundary, Focusable, Patch, afterPatch } from '@decky/ui';
|
||||
import { FC, ReactElement, ReactNode, cloneElement, createElement, memo } from 'react';
|
||||
import type { Route } from 'react-router';
|
||||
|
||||
@@ -41,13 +41,7 @@ class RouterHook extends Logger {
|
||||
window.__ROUTER_HOOK_INSTANCE?.deinit?.();
|
||||
window.__ROUTER_HOOK_INSTANCE = this;
|
||||
|
||||
this.gamepadWrapper = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.render?.toString()?.includes('["flow-children","onActivate","onCancel","focusClassName",'))
|
||||
return m[prop];
|
||||
}
|
||||
});
|
||||
this.gamepadWrapper = Focusable;
|
||||
|
||||
let Route: new () => Route;
|
||||
// Used to store the new replicated routes we create to allow routes to be unpatched.
|
||||
@@ -63,11 +57,11 @@ class RouterHook extends Logger {
|
||||
if (routes) {
|
||||
if (!routeList[routerIndex - 1]?.length || routeList[routerIndex - 1]?.length !== routes.size) {
|
||||
if (routeList[routerIndex - 1]?.length && routeList[routerIndex - 1].length !== routes.size) routerIndex--;
|
||||
const newRouterArray: ReactElement[] = [];
|
||||
const newRouterArray: (ReactElement | JSX.Element)[] = [];
|
||||
routes.forEach(({ component, props }, path) => {
|
||||
newRouterArray.push(
|
||||
<Route path={path} {...props}>
|
||||
{createElement(component)}
|
||||
<ErrorBoundary>{createElement(component)}</ErrorBoundary>
|
||||
</Route>,
|
||||
);
|
||||
});
|
||||
|
||||
+9
-37
@@ -3,24 +3,16 @@ import Backend from 'i18next-http-backend';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import PluginLoader from './plugin-loader';
|
||||
import { DeckyUpdater } from './updater';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
DeckyPluginLoader: PluginLoader;
|
||||
DeckyUpdater?: DeckyUpdater;
|
||||
importDeckyPlugin: Function;
|
||||
syncDeckyPlugins: Function;
|
||||
deckyHasLoaded: boolean;
|
||||
deckyHasConnectedRDT?: boolean;
|
||||
deckyAuthToken: string;
|
||||
DFL?: any;
|
||||
}
|
||||
export var DeckyPluginLoader: PluginLoader;
|
||||
export var deckyHasLoaded: boolean;
|
||||
export var deckyHasConnectedRDT: boolean | undefined;
|
||||
export var deckyAuthToken: string;
|
||||
export var DFL: any | undefined;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
window.deckyAuthToken = await fetch('http://127.0.0.1:1337/auth/token').then((r) => r.text());
|
||||
|
||||
i18n
|
||||
.use(Backend)
|
||||
.use(initReactI18next)
|
||||
@@ -40,7 +32,7 @@ declare global {
|
||||
backend: {
|
||||
loadPath: 'http://127.0.0.1:1337/locales/{{lng}}.json',
|
||||
customHeaders: {
|
||||
Authentication: window.deckyAuthToken,
|
||||
'X-Decky-Auth': deckyAuthToken,
|
||||
},
|
||||
requestOptions: {
|
||||
credentials: 'include',
|
||||
@@ -48,30 +40,10 @@ declare global {
|
||||
},
|
||||
});
|
||||
|
||||
window.DeckyPluginLoader?.dismountAll();
|
||||
window.DeckyPluginLoader?.deinit();
|
||||
window?.DeckyPluginLoader?.dismountAll();
|
||||
window?.DeckyPluginLoader?.deinit();
|
||||
window.DeckyPluginLoader = new PluginLoader();
|
||||
window.DeckyPluginLoader.init();
|
||||
window.importDeckyPlugin = function (name: string, version: string) {
|
||||
window.DeckyPluginLoader?.importPlugin(name, version);
|
||||
};
|
||||
|
||||
window.syncDeckyPlugins = async function () {
|
||||
const plugins = await (
|
||||
await fetch('http://127.0.0.1:1337/plugins', {
|
||||
credentials: 'include',
|
||||
headers: { Authentication: window.deckyAuthToken },
|
||||
})
|
||||
).json();
|
||||
for (const plugin of plugins) {
|
||||
if (!window.DeckyPluginLoader.hasPlugin(plugin.name))
|
||||
window.DeckyPluginLoader?.importPlugin(plugin.name, plugin.version);
|
||||
}
|
||||
|
||||
window.DeckyPluginLoader.checkPluginUpdates();
|
||||
};
|
||||
|
||||
setTimeout(() => window.syncDeckyPlugins(), 5000);
|
||||
DeckyPluginLoader.init();
|
||||
})();
|
||||
|
||||
export default i18n;
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { getFocusNavController, sleep } from 'decky-frontend-lib';
|
||||
|
||||
import Logger from '../logger';
|
||||
|
||||
const logger = new Logger('ReloadSteamFix');
|
||||
|
||||
declare global {
|
||||
var GamepadNavTree: any;
|
||||
}
|
||||
|
||||
export default async function reloadFix() {
|
||||
// Hack to unbreak the ui when reloading it
|
||||
await sleep(4000);
|
||||
if (getFocusNavController()?.m_rgAllContexts?.length == 0) {
|
||||
SteamClient.URL.ExecuteSteamURL('steam://open/settings');
|
||||
logger.log('Applied UI reload fix.');
|
||||
}
|
||||
|
||||
// This steamfix does not need to deinit.
|
||||
return () => {};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Patch, findModuleChild, replacePatch, sleep } from 'decky-frontend-lib';
|
||||
import { Export, Patch, findModuleExport, replacePatch, sleep } from '@decky/ui';
|
||||
|
||||
import Logger from '../logger';
|
||||
|
||||
@@ -18,12 +18,7 @@ export default async function restartFix() {
|
||||
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);
|
||||
|
||||
+28
-17
@@ -1,4 +1,4 @@
|
||||
import { InstallType, Plugin } from './plugin';
|
||||
import { InstallType, Plugin, installPlugin, installPlugins } from './plugin';
|
||||
import { getSetting, setSetting } from './utils/settings';
|
||||
|
||||
export enum Store {
|
||||
@@ -53,7 +53,6 @@ export async function getPluginList(
|
||||
): Promise<StorePlugin[]> {
|
||||
let version = await window.DeckyPluginLoader.updateVersion();
|
||||
let store = await getSetting<Store | null>('store', null);
|
||||
|
||||
let customURL = await getSetting<string>('store-url', 'https://plugins.deckbrew.xyz/plugins');
|
||||
|
||||
let query: URLSearchParams | string = new URLSearchParams();
|
||||
@@ -67,6 +66,27 @@ export async function getPluginList(
|
||||
await setSetting('store', Store.Default);
|
||||
store = Store.Default;
|
||||
}
|
||||
switch (+store) {
|
||||
case Store.Default:
|
||||
storeURL = 'https://plugins.deckbrew.xyz/plugins';
|
||||
break;
|
||||
case Store.Testing:
|
||||
storeURL = 'https://testing.deckbrew.xyz/plugins';
|
||||
break;
|
||||
case Store.Custom:
|
||||
storeURL = customURL;
|
||||
break;
|
||||
default:
|
||||
console.error('Somehow you ended up without a standard URL, using the default URL.');
|
||||
storeURL = 'https://plugins.deckbrew.xyz/plugins';
|
||||
break;
|
||||
return fetch(storeURL, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Decky-Version': version.current,
|
||||
},
|
||||
}).then((r) => r.json());
|
||||
}
|
||||
switch (+store) {
|
||||
case Store.Default:
|
||||
storeURL = 'https://plugins.deckbrew.xyz/plugins';
|
||||
@@ -92,36 +112,27 @@ export async function getPluginList(
|
||||
|
||||
export async function installFromURL(url: string) {
|
||||
const splitURL = url.split('/');
|
||||
await window.DeckyPluginLoader.callServerMethod('install_plugin', {
|
||||
name: splitURL[splitURL.length - 1].replace('.zip', ''),
|
||||
artifact: url,
|
||||
});
|
||||
await installPlugin(url, splitURL[splitURL.length - 1].replace('.zip', ''));
|
||||
}
|
||||
|
||||
export async function requestPluginInstall(plugin: string, selectedVer: StorePluginVersion, installType: InstallType) {
|
||||
const artifactUrl = selectedVer.artifact ?? pluginUrl(selectedVer.hash);
|
||||
await window.DeckyPluginLoader.callServerMethod('install_plugin', {
|
||||
name: plugin,
|
||||
artifact: artifactUrl,
|
||||
version: selectedVer.name,
|
||||
hash: selectedVer.hash,
|
||||
install_type: installType,
|
||||
});
|
||||
await installPlugin(artifactUrl, plugin, selectedVer.name, selectedVer.hash, installType);
|
||||
}
|
||||
|
||||
export async function requestMultiplePluginInstalls(requests: PluginInstallRequest[]) {
|
||||
await window.DeckyPluginLoader.callServerMethod('install_plugins', {
|
||||
requests: requests.map(({ plugin, installType, selectedVer }) => ({
|
||||
await installPlugins(
|
||||
requests.map(({ plugin, installType, selectedVer }) => ({
|
||||
name: plugin,
|
||||
artifact: selectedVer.artifact ?? pluginUrl(selectedVer.hash),
|
||||
version: selectedVer.name,
|
||||
hash: selectedVer.hash,
|
||||
install_type: installType,
|
||||
})),
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
export async function checkForUpdates(plugins: Plugin[]): Promise<PluginUpdateMapping> {
|
||||
export async function checkForPluginUpdates(plugins: Plugin[]): Promise<PluginUpdateMapping> {
|
||||
const serverData = await getPluginList();
|
||||
const updateMap = new Map<string, StorePluginVersion>();
|
||||
for (let plugin of plugins) {
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
// TabsHook for versions before the Desktop merge
|
||||
import { Patch, afterPatch, getReactRoot, sleep } from 'decky-frontend-lib';
|
||||
import { memo } from 'react';
|
||||
|
||||
import NewTabsHook from './tabs-hook';
|
||||
|
||||
declare global {
|
||||
interface Array<T> {
|
||||
__filter: any;
|
||||
}
|
||||
}
|
||||
|
||||
const isTabsArray = (tabs: any) => {
|
||||
const length = tabs.length;
|
||||
return length >= 7 && tabs[length - 1]?.tab;
|
||||
};
|
||||
|
||||
class TabsHook extends NewTabsHook {
|
||||
// private keys = 7;
|
||||
private quickAccess: any;
|
||||
private tabRenderer: any;
|
||||
private memoizedQuickAccess: any;
|
||||
private cNode: any;
|
||||
|
||||
private qAPTree: any;
|
||||
private rendererTree: any;
|
||||
|
||||
private cNodePatch?: Patch;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.log('Initialized stable TabsHook');
|
||||
}
|
||||
|
||||
init() {
|
||||
const self = this;
|
||||
const tree = getReactRoot(document.getElementById('root') as any);
|
||||
let scrollRoot: any;
|
||||
async function findScrollRoot(currentNode: any, iters: number): Promise<any> {
|
||||
if (iters >= 30) {
|
||||
self.error(
|
||||
'Scroll root was not found before hitting the recursion limit, a developer will need to increase the limit.',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
currentNode = currentNode?.child;
|
||||
if (currentNode?.type?.prototype?.RemoveSmartScrollContainer) {
|
||||
self.log(`Scroll root was found in ${iters} recursion cycles`);
|
||||
return currentNode;
|
||||
}
|
||||
if (!currentNode) return null;
|
||||
if (currentNode.sibling) {
|
||||
let node = await findScrollRoot(currentNode.sibling, iters + 1);
|
||||
if (node !== null) return node;
|
||||
}
|
||||
return await findScrollRoot(currentNode, iters + 1);
|
||||
}
|
||||
(async () => {
|
||||
scrollRoot = await findScrollRoot(tree, 0);
|
||||
while (!scrollRoot) {
|
||||
this.log('Failed to find scroll root node, reattempting in 5 seconds');
|
||||
await sleep(5000);
|
||||
scrollRoot = await findScrollRoot(tree, 0);
|
||||
}
|
||||
let newQA: any;
|
||||
let newQATabRenderer: any;
|
||||
this.cNodePatch = afterPatch(scrollRoot.stateNode, 'render', (_: any, ret: any) => {
|
||||
if (!this.quickAccess && ret.props.children.props.children[4]) {
|
||||
this.quickAccess = ret?.props?.children?.props?.children[4].type;
|
||||
newQA = (...args: any) => {
|
||||
const ret = this.quickAccess.type(...args);
|
||||
if (ret) {
|
||||
if (!newQATabRenderer) {
|
||||
this.tabRenderer = ret.props.children[1].children.type;
|
||||
newQATabRenderer = (...qamArgs: any[]) => {
|
||||
const oFilter = Array.prototype.filter;
|
||||
Array.prototype.filter = function (...args: any[]) {
|
||||
if (isTabsArray(this)) {
|
||||
self.render(this, qamArgs[0].visible);
|
||||
}
|
||||
// @ts-ignore
|
||||
return oFilter.call(this, ...args);
|
||||
};
|
||||
// TODO remove array hack entirely and use this instead const tabs = ret.props.children.props.children[0].props.children[1].props.children[0].props.children[0].props.tabs
|
||||
const ret = this.tabRenderer(...qamArgs);
|
||||
Array.prototype.filter = oFilter;
|
||||
return ret;
|
||||
};
|
||||
}
|
||||
this.rendererTree = ret.props.children[1].children;
|
||||
ret.props.children[1].children.type = newQATabRenderer;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
this.memoizedQuickAccess = memo(newQA);
|
||||
this.memoizedQuickAccess.isDeckyQuickAccess = true;
|
||||
}
|
||||
if (ret.props.children.props.children[4]) {
|
||||
this.qAPTree = ret.props.children.props.children[4];
|
||||
ret.props.children.props.children[4].type = this.memoizedQuickAccess;
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
this.cNode = scrollRoot;
|
||||
this.cNode.stateNode.forceUpdate();
|
||||
this.log('Finished initial injection');
|
||||
})();
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.cNodePatch?.unpatch();
|
||||
if (this.qAPTree) this.qAPTree.type = this.quickAccess;
|
||||
if (this.rendererTree) this.rendererTree.type = this.tabRenderer;
|
||||
if (this.cNode) this.cNode.stateNode.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
export default TabsHook;
|
||||
@@ -1,5 +1,5 @@
|
||||
// TabsHook for versions after the Desktop merge
|
||||
import { Patch, QuickAccessTab, afterPatch, findInReactTree, getReactRoot, sleep } from 'decky-frontend-lib';
|
||||
import { ErrorBoundary, Patch, QuickAccessTab, afterPatch, findInReactTree, getReactRoot, sleep } from '@decky/ui';
|
||||
|
||||
import { QuickAccessVisibleStateProvider } from './components/QuickAccessVisibleState';
|
||||
import Logger from './logger';
|
||||
@@ -147,7 +147,11 @@ class TabsHook extends Logger {
|
||||
decky: true,
|
||||
initialVisibility: visible,
|
||||
};
|
||||
tab.panel = <QuickAccessVisibleStateProvider tab={tab}>{content}</QuickAccessVisibleStateProvider>;
|
||||
tab.panel = (
|
||||
<ErrorBoundary>
|
||||
<QuickAccessVisibleStateProvider tab={tab}>{content}</QuickAccessVisibleStateProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
existingTabs.push(tab);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { ToastData } from '@decky/api';
|
||||
import {
|
||||
Module,
|
||||
Export,
|
||||
Patch,
|
||||
ToastData,
|
||||
afterPatch,
|
||||
findClass,
|
||||
findInReactTree,
|
||||
findModuleChild,
|
||||
findModuleExport,
|
||||
getReactRoot,
|
||||
sleep,
|
||||
} from 'decky-frontend-lib';
|
||||
} from '@decky/ui';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import Toast from './components/Toast';
|
||||
@@ -79,7 +79,7 @@ class Toaster extends Logger {
|
||||
};
|
||||
instance = findToasterRoot(tree, 0);
|
||||
while (!instance) {
|
||||
this.error(
|
||||
this.warn(
|
||||
'Failed to find Toaster root node, reattempting in 5 seconds. A developer may need to increase the recursion limit.',
|
||||
);
|
||||
await sleep(5000);
|
||||
@@ -124,12 +124,12 @@ class Toaster extends Logger {
|
||||
this.node.alternate.type = this.node.type;
|
||||
}
|
||||
};
|
||||
const oRender = this.rNode.stateNode.__proto__.render;
|
||||
let int: NodeJS.Timer | undefined;
|
||||
const oRender = Object.getPrototypeOf(this.rNode.stateNode).render;
|
||||
let int: number | undefined;
|
||||
this.rNode.stateNode.render = (...args: any[]) => {
|
||||
const ret = oRender.call(this.rNode.stateNode, ...args);
|
||||
if (ret && !this?.node?.return?.return) {
|
||||
clearInterval(int);
|
||||
int && clearInterval(int);
|
||||
int = setInterval(() => {
|
||||
const n = findToasterRoot(tree, 0);
|
||||
if (n?.return) {
|
||||
@@ -150,16 +150,7 @@ class Toaster extends Logger {
|
||||
this.rNode.stateNode.forceUpdate();
|
||||
delete this.rNode.stateNode.shouldComponentUpdate;
|
||||
|
||||
this.audioModule = findModuleChild((m: Module) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
try {
|
||||
if (m[prop].PlayNavSound && m[prop].RegisterCallbackOnPlaySound) return m[prop];
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
this.audioModule = findModuleExport((e: Export) => e.PlayNavSound && e.RegisterCallbackOnPlaySound);
|
||||
|
||||
this.log('Initialized');
|
||||
this.finishStartup?.();
|
||||
|
||||
+5
-22
@@ -4,11 +4,6 @@ export enum Branches {
|
||||
// Testing,
|
||||
}
|
||||
|
||||
export interface DeckyUpdater {
|
||||
updateProgress: (val: number) => void;
|
||||
finish: () => void;
|
||||
}
|
||||
|
||||
export interface RemoteVerInfo {
|
||||
assets: {
|
||||
browser_download_url: string;
|
||||
@@ -28,20 +23,8 @@ export interface VerInfo {
|
||||
updatable: boolean;
|
||||
}
|
||||
|
||||
export async function callUpdaterMethod(methodName: string, args = {}) {
|
||||
const response = await fetch(`http://127.0.0.1:1337/updater/${methodName}`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authentication: window.deckyAuthToken,
|
||||
},
|
||||
body: JSON.stringify(args),
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function finishUpdate() {
|
||||
callUpdaterMethod('do_restart');
|
||||
}
|
||||
export const doUpdate = DeckyBackend.callable('updater/do_update');
|
||||
export const doRestart = DeckyBackend.callable('updater/do_restart');
|
||||
export const doShutdown = DeckyBackend.callable('updater/do_shutdown');
|
||||
export const getVersionInfo = DeckyBackend.callable<[], VerInfo>('updater/get_version_info');
|
||||
export const checkForUpdates = DeckyBackend.callable<[], VerInfo>('updater/check_for_updates');
|
||||
|
||||
@@ -11,47 +11,42 @@ export enum TranslationClass {
|
||||
}
|
||||
|
||||
interface TranslationHelperProps {
|
||||
trans_class: TranslationClass;
|
||||
trans_text: string;
|
||||
i18n_args?: {};
|
||||
install_type?: number;
|
||||
transClass: TranslationClass;
|
||||
transText: string;
|
||||
i18nArgs?: {};
|
||||
installType?: number;
|
||||
}
|
||||
|
||||
const logger = new Logger('TranslationHelper');
|
||||
|
||||
const TranslationHelper: FC<TranslationHelperProps> = ({
|
||||
trans_class,
|
||||
trans_text,
|
||||
i18n_args = null,
|
||||
install_type = 0,
|
||||
}) => {
|
||||
const TranslationHelper: FC<TranslationHelperProps> = ({ transClass, transText, i18nArgs = null, installType = 0 }) => {
|
||||
return (
|
||||
<Translation>
|
||||
{(t, {}) => {
|
||||
switch (trans_class) {
|
||||
switch (transClass) {
|
||||
case TranslationClass.PLUGIN_LOADER:
|
||||
return i18n_args
|
||||
? t(TranslationClass.PLUGIN_LOADER + '.' + trans_text, i18n_args)
|
||||
: t(TranslationClass.PLUGIN_LOADER + '.' + trans_text);
|
||||
return i18nArgs
|
||||
? t(TranslationClass.PLUGIN_LOADER + '.' + transText, i18nArgs)
|
||||
: t(TranslationClass.PLUGIN_LOADER + '.' + transText);
|
||||
case TranslationClass.PLUGIN_INSTALL_MODAL:
|
||||
switch (install_type) {
|
||||
switch (installType) {
|
||||
case InstallType.INSTALL:
|
||||
return i18n_args
|
||||
? t(TranslationClass.PLUGIN_INSTALL_MODAL + '.install.' + trans_text, i18n_args)
|
||||
: t(TranslationClass.PLUGIN_INSTALL_MODAL + '.install.' + trans_text);
|
||||
return i18nArgs
|
||||
? t(TranslationClass.PLUGIN_INSTALL_MODAL + '.install.' + transText, i18nArgs)
|
||||
: t(TranslationClass.PLUGIN_INSTALL_MODAL + '.install.' + transText);
|
||||
case InstallType.REINSTALL:
|
||||
return i18n_args
|
||||
? t(TranslationClass.PLUGIN_INSTALL_MODAL + '.reinstall.' + trans_text, i18n_args)
|
||||
: t(TranslationClass.PLUGIN_INSTALL_MODAL + '.reinstall.' + trans_text);
|
||||
return i18nArgs
|
||||
? t(TranslationClass.PLUGIN_INSTALL_MODAL + '.reinstall.' + transText, i18nArgs)
|
||||
: t(TranslationClass.PLUGIN_INSTALL_MODAL + '.reinstall.' + transText);
|
||||
case InstallType.UPDATE:
|
||||
return i18n_args
|
||||
? t(TranslationClass.PLUGIN_INSTALL_MODAL + '.update.' + trans_text, i18n_args)
|
||||
: t(TranslationClass.PLUGIN_INSTALL_MODAL + '.update.' + trans_text);
|
||||
return i18nArgs
|
||||
? t(TranslationClass.PLUGIN_INSTALL_MODAL + '.update.' + transText, i18nArgs)
|
||||
: t(TranslationClass.PLUGIN_INSTALL_MODAL + '.update.' + transText);
|
||||
}
|
||||
case TranslationClass.DEVELOPER:
|
||||
return i18n_args
|
||||
? t(TranslationClass.DEVELOPER + '.' + trans_text, i18n_args)
|
||||
: t(TranslationClass.DEVELOPER + '.' + trans_text);
|
||||
return i18nArgs
|
||||
? t(TranslationClass.DEVELOPER + '.' + transText, i18nArgs)
|
||||
: t(TranslationClass.DEVELOPER + '.' + transText);
|
||||
default:
|
||||
logger.error('We should never fall in the default case!');
|
||||
return '';
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { ErrorInfo } from 'react';
|
||||
|
||||
const pluginErrorRegex = /http:\/\/localhost:1337\/plugins\/([^\/]*)\//;
|
||||
const pluginSourceMapErrorRegex = /decky:\/\/decky\/plugin\/([^\/]*)\//;
|
||||
const legacyPluginErrorRegex = /decky:\/\/decky\/legacy_plugin\/([^\/]*)\/index.js/;
|
||||
|
||||
export interface ValveReactErrorInfo {
|
||||
error: Error;
|
||||
info: ErrorInfo;
|
||||
}
|
||||
|
||||
export interface ValveError {
|
||||
identifier: string;
|
||||
identifierHash: string;
|
||||
message: string | [func: string, src: string, line: number, column: number];
|
||||
}
|
||||
|
||||
export type ErrorSource = [source: string, wasPlugin: boolean, shouldReportToValve: boolean];
|
||||
|
||||
export function getLikelyErrorSourceFromValveError(error: ValveError): ErrorSource {
|
||||
return getLikelyErrorSource(JSON.stringify(error?.message));
|
||||
}
|
||||
|
||||
export function getLikelyErrorSourceFromValveReactError(error: ValveReactErrorInfo): ErrorSource {
|
||||
return getLikelyErrorSource(error?.error?.stack + '\n' + error.info.componentStack);
|
||||
}
|
||||
|
||||
export function getLikelyErrorSource(error?: string): ErrorSource {
|
||||
const pluginMatch = error?.match(pluginErrorRegex);
|
||||
if (pluginMatch) {
|
||||
return [decodeURIComponent(pluginMatch[1]), true, false];
|
||||
}
|
||||
|
||||
const pluginMatchViaMap = error?.match(pluginSourceMapErrorRegex);
|
||||
if (pluginMatchViaMap) {
|
||||
return [decodeURIComponent(pluginMatchViaMap[1]), true, false];
|
||||
}
|
||||
|
||||
const legacyPluginMatch = error?.match(legacyPluginErrorRegex);
|
||||
if (legacyPluginMatch) {
|
||||
return [decodeURIComponent(legacyPluginMatch[1]), true, false];
|
||||
}
|
||||
|
||||
if (error?.includes('http://localhost:1337/')) {
|
||||
return ['the Decky frontend', false, false];
|
||||
}
|
||||
return ['Steam', false, true];
|
||||
}
|
||||
@@ -1,24 +1,8 @@
|
||||
interface GetSettingArgs<T> {
|
||||
key: string;
|
||||
default: T;
|
||||
}
|
||||
|
||||
interface SetSettingArgs<T> {
|
||||
key: string;
|
||||
value: T;
|
||||
}
|
||||
|
||||
export async function getSetting<T>(key: string, def: T): Promise<T> {
|
||||
const res = (await window.DeckyPluginLoader.callServerMethod('get_setting', {
|
||||
key,
|
||||
default: def,
|
||||
} as GetSettingArgs<T>)) as { result: T };
|
||||
return res.result;
|
||||
const res = await DeckyBackend.call<[string, T], T>('utilities/settings/get', key, def);
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function setSetting<T>(key: string, value: T): Promise<void> {
|
||||
await window.DeckyPluginLoader.callServerMethod('set_setting', {
|
||||
key,
|
||||
value,
|
||||
} as SetSettingArgs<T>);
|
||||
await DeckyBackend.call<[string, T], void>('utilities/settings/set', key, value);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
import { sleep } from '@decky/ui';
|
||||
|
||||
import Logger from './logger';
|
||||
|
||||
declare global {
|
||||
export var DeckyBackend: WSRouter;
|
||||
}
|
||||
|
||||
enum MessageType {
|
||||
ERROR = -1,
|
||||
// Call-reply, Frontend -> Backend -> Frontend
|
||||
CALL = 0,
|
||||
REPLY = 1,
|
||||
// Pub/Sub, Backend -> Frontend
|
||||
EVENT = 3,
|
||||
}
|
||||
|
||||
interface CallMessage {
|
||||
type: MessageType.CALL;
|
||||
args: any[];
|
||||
route: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface ReplyMessage {
|
||||
type: MessageType.REPLY;
|
||||
result: any;
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface ErrorMessage {
|
||||
type: MessageType.ERROR;
|
||||
error: { name: string; error: string; traceback: string | null };
|
||||
id: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* An error from a python call
|
||||
*/
|
||||
export class PyError extends Error {
|
||||
pythonTraceback: string | null;
|
||||
|
||||
constructor(name: string, error: string, traceback: string | null) {
|
||||
super(error);
|
||||
this.name = `Python ${name}`;
|
||||
if (traceback) {
|
||||
// traceback will always start with `Traceback (most recent call last):`
|
||||
// so this will make it say `Python Traceback (most recent call last):` after the JS callback
|
||||
this.stack = this.stack + '\n\nPython ' + traceback;
|
||||
}
|
||||
this.pythonTraceback = traceback;
|
||||
}
|
||||
}
|
||||
|
||||
interface EventMessage {
|
||||
type: MessageType.EVENT;
|
||||
event: string;
|
||||
args: any;
|
||||
}
|
||||
|
||||
type Message = CallMessage | ReplyMessage | ErrorMessage | EventMessage;
|
||||
|
||||
// Helper to resolve a promise from the outside
|
||||
interface PromiseResolver<T> {
|
||||
resolve: (res: T) => void;
|
||||
reject: (error: PyError) => void;
|
||||
promise: Promise<T>;
|
||||
}
|
||||
|
||||
export class WSRouter extends Logger {
|
||||
runningCalls: Map<number, PromiseResolver<any>> = new Map();
|
||||
eventListeners: Map<string, Set<(...args: any) => any>> = new Map();
|
||||
ws?: WebSocket;
|
||||
connectPromise?: Promise<void>;
|
||||
// Used to map results and errors to calls
|
||||
reqId: number = 0;
|
||||
constructor() {
|
||||
super('WSRouter');
|
||||
}
|
||||
|
||||
connect() {
|
||||
return (this.connectPromise = new Promise<void>((resolve) => {
|
||||
// Auth is a query param as JS WebSocket doesn't support headers
|
||||
this.ws = new WebSocket(`ws://127.0.0.1:1337/ws?auth=${deckyAuthToken}`);
|
||||
|
||||
this.ws.addEventListener('open', () => {
|
||||
this.debug('WS Connected');
|
||||
resolve();
|
||||
delete this.connectPromise;
|
||||
});
|
||||
this.ws.addEventListener('message', this.onMessage.bind(this));
|
||||
this.ws.addEventListener('close', this.onError.bind(this));
|
||||
// this.ws.addEventListener('error', this.onError.bind(this));
|
||||
}));
|
||||
}
|
||||
|
||||
createPromiseResolver<T>(): PromiseResolver<T> {
|
||||
let resolver: Partial<PromiseResolver<T>> = {};
|
||||
const promise = new Promise<T>((resolve, reject) => {
|
||||
resolver.resolve = resolve;
|
||||
resolver.reject = reject;
|
||||
});
|
||||
resolver.promise = promise;
|
||||
return resolver as PromiseResolver<T>;
|
||||
}
|
||||
|
||||
async write(data: Message) {
|
||||
if (this.connectPromise) await this.connectPromise;
|
||||
this.ws?.send(JSON.stringify(data));
|
||||
}
|
||||
|
||||
addEventListener(event: string, listener: (...args: any) => any) {
|
||||
if (!this.eventListeners.has(event)) {
|
||||
this.eventListeners.set(event, new Set([listener]));
|
||||
} else {
|
||||
this.eventListeners.get(event)?.add(listener);
|
||||
}
|
||||
return listener;
|
||||
}
|
||||
|
||||
removeEventListener(event: string, listener: (...args: any) => any) {
|
||||
if (this.eventListeners.has(event)) {
|
||||
const set = this.eventListeners.get(event);
|
||||
set?.delete(listener);
|
||||
if (set?.size === 0) {
|
||||
this.eventListeners.delete(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async onMessage(msg: MessageEvent) {
|
||||
try {
|
||||
const data = JSON.parse(msg.data) as Message;
|
||||
switch (data.type) {
|
||||
case MessageType.REPLY:
|
||||
if (this.runningCalls.has(data.id)) {
|
||||
this.runningCalls.get(data.id)!.resolve(data.result);
|
||||
this.runningCalls.delete(data.id);
|
||||
this.debug(`[${data.id}] Resolved PY call with value`, data.result);
|
||||
}
|
||||
break;
|
||||
|
||||
case MessageType.ERROR:
|
||||
if (this.runningCalls.has(data.id)) {
|
||||
let err = new PyError(data.error.name, data.error.error, data.error.traceback);
|
||||
this.runningCalls.get(data.id)!.reject(err);
|
||||
this.runningCalls.delete(data.id);
|
||||
this.debug(`[${data.id}] Rejected PY call with error`, data.error);
|
||||
}
|
||||
break;
|
||||
|
||||
case MessageType.EVENT:
|
||||
this.debug(`Recieved event ${data.event} with args`, data.args);
|
||||
if (this.eventListeners.has(data.event)) {
|
||||
for (const listener of this.eventListeners.get(data.event)!) {
|
||||
(async () => {
|
||||
try {
|
||||
await listener(...data.args);
|
||||
} catch (e) {
|
||||
this.error(`error in event ${data.event}`, e, listener);
|
||||
}
|
||||
})();
|
||||
}
|
||||
} else {
|
||||
this.warn(`event ${data.event} has no listeners`);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
this.error('Unknown message type', data);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
this.error('Error parsing WebSocket message', e);
|
||||
}
|
||||
}
|
||||
|
||||
// this.call<[number, number], string>('methodName', 1, 2);
|
||||
call<Args extends any[] = [], Return = void>(route: string, ...args: Args): Promise<Return> {
|
||||
const resolver = this.createPromiseResolver<Return>();
|
||||
|
||||
const id = ++this.reqId;
|
||||
|
||||
this.runningCalls.set(id, resolver);
|
||||
|
||||
this.debug(`[${id}] Calling PY method ${route} with args`, args);
|
||||
|
||||
this.write({ type: MessageType.CALL, route, args, id });
|
||||
|
||||
return resolver.promise;
|
||||
}
|
||||
|
||||
callable<Args extends any[] = [], Return = void>(route: string): (...args: Args) => Promise<Return> {
|
||||
return (...args) => this.call<Args, Return>(route, ...args);
|
||||
}
|
||||
|
||||
async onError(error: any) {
|
||||
this.error('WS DISCONNECTED', error);
|
||||
// TODO queue up lost messages and send them once we connect again
|
||||
await sleep(5000);
|
||||
await this.connect();
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@
|
||||
"noImplicitAny": true,
|
||||
"strict": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src", "index.d.ts"],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user