Compare commits

...

70 Commits

Author SHA1 Message Date
AAGaming 6e88c7c9ac Show warning when installing legacy plugins 2022-07-17 16:09:42 -04:00
AAGaming f015e00561 more updater fixes 2022-07-15 12:57:51 -04:00
AAGaming e07827cdb5 catch rm errors 2022-07-15 12:36:16 -04:00
AAGaming 103d43e7c9 fix updater 2022-07-15 12:31:30 -04:00
AAGaming 23b7df0ce2 wait 30s before first update check 2022-07-15 12:25:27 -04:00
AAGaming a5671e19ce fix ci AGAIN 2022-07-15 12:20:05 -04:00
AAGaming f2fbd399fe allow users to manually check for updates 2022-07-15 12:16:57 -04:00
AAGaming 28b91963a9 fix ci part 4
fix ci part 5

fix ci part 6

fix ci part 7

fix ci part 8

fix ci part 9

fix ci part 10

fix ci part 11
2022-07-15 11:55:39 -04:00
AAGaming ce2268370f this slipped through the linter 2022-07-15 10:53:41 -04:00
AAGaming 59462041b1 fix ci part 3: shopt edition 2022-07-15 10:52:08 -04:00
AAGaming d4d32c8d55 fix ci part 2 2022-07-15 10:45:23 -04:00
AAGaming e600aeccc7 fix ci startup failure 2022-07-15 10:38:03 -04:00
AAGaming 162d1b561b fix lockup in _open_socket_if_not_exists, probably fix ci prereleases 2022-07-15 10:34:47 -04:00
Brian Choy ba824fc921 Fix jq errors in prerelease script (#118)
* Fix jq errors in prerelease script

* Use multivariable output, add back RELEASE var
2022-07-15 09:12:07 -04:00
AAGaming 8c8cf180fa Updater for decky-loader (#117)
* Add an updater in settings for decky-loader

* add chmod

* remove junk comments
2022-07-14 22:51:55 -04:00
AAGaming 05d11cfff0 fix get_tabs oopsie 2022-07-13 23:24:29 -04:00
botato 3c24b37247 change ci again (#116) 2022-07-13 21:19:19 -04:00
AAGaming dbb4bc5ab4 another CI fix from botato 2022-07-12 12:11:42 -04:00
botato b00b04ceeb Fix action not detecting prerelease 2022-07-11 17:41:11 -04:00
botato 470f16adda CI revamp (#110)
* ci: automatically make releases, ...

- option to run manually (for full-fledged releases)
- cron schedule for pre-releases (every day at 1 pm UTC)
- semantic versioning
- Automatically generated release description

* formatting

* more formatting .-.

* Tweak according to latest release
2022-07-11 09:13:56 +02:00
botato 76424174ed Use call instead of Popen (#113) 2022-07-11 08:56:36 +02:00
AAGaming b618fe1e97 bump lib 2022-07-07 00:03:56 -04:00
AAGaming 45949e8456 support non-ui plugins 2022-07-07 00:03:20 -04:00
TrainDoctor e3a965329d Update install_prerelease.sh 2022-07-04 08:27:44 -07:00
TrainDoctor 6ee41578ea Update plugin-loader.tsx 2022-07-03 16:56:35 -07:00
TrainDoctor 9404215399 Make legacy tag text readable 2022-07-03 16:18:07 -07:00
AAGaming b8bf150a74 fix legacy coloring 2022-07-03 19:12:10 -04:00
AAGaming add3f77c1a colorize legacy tag 2022-07-03 18:48:58 -04:00
AAGaming 6c42661f86 hack: temp hide example plugin 2022-07-03 17:37:39 -04:00
TrainDoctor 2b3c219e38 * Async onOK
* await confirm_plugin_install

* wait until we've exited store to re-open QAM
2022-07-03 14:28:48 -07:00
TrainDoctor 8eb89da373 Update README.md 2022-07-03 13:30:58 -07:00
TrainDoctor ace9f61e50 Redirect to QAM after installing a plugin, QOL. 2022-07-03 12:52:22 -07:00
WerWolv baa02c129f Fixed plugin installation ssl verification issue (#101)
* Added cert location debugging

* Install certifi

* Try adding manual cacert in install request

* Properly use ssl

* More efficiently load ssl certificate
2022-07-03 08:29:46 +02:00
TrainDoctor 1e6b3edbf2 Merge remote-tracking branch 'origin/main' 2022-07-02 23:14:51 -07:00
botato 085aacea06 Use deckyState in uninstall menu (fixes #98) (#100) 2022-07-02 22:14:43 -04:00
TrainDoctor 675e667a9e Catch uninstall plugin 2022-07-02 17:09:21 -07:00
TrainDoctor 58b2c4208d Remove bugged rename invocation 2022-07-02 16:37:23 -07:00
TrainDoctor c2693869a7 Fix debug logging 2022-07-02 16:04:09 -07:00
TrainDoctor 683c51ceac Properly await uninstall 2022-07-02 15:59:15 -07:00
TrainDoctor 630e8b7213 Update prerelease script 2022-07-02 15:37:20 -07:00
TrainDoctor 246b31794a Update workflow 2022-07-02 14:55:27 -07:00
TrainDoctor b7d57de378 Add pre-release install script 2022-07-02 14:42:41 -07:00
TrainDoctor ee8aa98446 Update README.md, password is needed (#70)
(cherry picked from commit 1199c080bc)
Added some context and changed wording on uninstall.
2022-07-02 12:41:25 -07:00
TrainDoctor 557a00aed7 Update README.md 2022-07-01 17:15:32 -07:00
botato 4daf028e7a Uninstall functionality (#97)
* feat: POC uninstallation feature

* Fixes, placeholder

* bugfix: wrong function call

* add oncancel and change function called

* clean up plugin uninstall code

* bugfix, uninstall in store

* Limit scope of feature branch

* feat: PluginLoader.unloadPlugin

* problematic logs
2022-07-01 16:43:17 -07:00
AAGaming 934a50f683 fix legacy plugin duplication 2022-07-01 11:50:08 -04:00
TrainDoctor aa4f1b1e87 pnpm update 2022-06-30 15:15:15 -07:00
AAGaming 67495d30d6 fix packager 2022-06-30 16:48:49 -04:00
AAGaming d72f364a8d backwards-compatible plugin store, legacy plugin library 2022-06-30 16:04:29 -04:00
TrainDoctor da0f7dd337 Tone down hash missing warning. 2022-06-29 12:23:11 -07:00
TrainDoctor 518b01f571 Installing from plugin store now works as intended 2022-06-29 11:46:06 -07:00
AAGaming 3f2a2bbc04 fix installing plugins 2022-06-29 12:25:50 -04:00
AAGaming 79e8af8be6 update store for new format, awaiting cors to test 2022-06-29 12:17:25 -04:00
AAGaming 18d444e8fc fix tab type, bump lib for tree shaking 2022-06-29 11:57:59 -04:00
hulkrelax abc5ce5382 remove body property in args (#91) 2022-06-28 21:12:55 -04:00
AAGaming 9619c52720 add settings page with install from URL option 2022-06-22 23:22:27 -04:00
TrainDoctor 80b223180e Remove old info and redirect to wiki for in-development info 2022-06-21 14:32:02 -07:00
TrainDoctor 1d5d14b492 Added remote launch option 2022-06-21 13:49:12 -07:00
TrainDoctor ce23534ccc Remove argument parity between scripts, not sustainable solution 2022-06-21 12:36:43 -07:00
AAGaming e6e74d8e9d Don't allow overriding name 2022-06-21 09:52:54 -04:00
TrainDoctor 6289578f68 Update pnpm-lock.yaml 2022-06-20 20:40:50 -07:00
AAGaming e7c44ee202 Replace tabs hook, fix panels, bump lib 2022-06-20 23:37:52 -04:00
TrainDoctor 39f6a7688d Converted install script to pnpm 2022-06-20 20:24:44 -07:00
TrainDoctor 47ca3ece4a Added python depdency install, fixed use-case phrasing 2022-06-20 18:56:22 -07:00
Jonas Dellinger 3e250dd180 Fix importPlugin queue 2022-06-20 15:54:31 +02:00
Jonas Dellinger 711af3bca3 Fix onDismount 2022-06-20 15:34:08 +02:00
Jonas Dellinger 9a6930571c Fix onDismount 2022-06-20 15:29:40 +02:00
Jonas Dellinger d9dd09c69b Revert "fix onDismount"
This reverts commit daca482ed8.
2022-06-20 15:28:30 +02:00
AAGaming daca482ed8 fix onDismount 2022-06-19 18:56:02 -04:00
AAGaming 99b4b939bd Implement React-based plugin store (#81)
Co-authored-by: TrainDoctor <11465594+TrainDoctor@users.noreply.github.com>
Co-authored-by: WerWolv <werwolv98@gmail.com>
2022-06-17 18:43:53 -04:00
38 changed files with 3404 additions and 4199 deletions
+128 -19
View File
@@ -2,52 +2,161 @@ name: Builder
on:
push:
branches: [ "*" ]
pull_request:
branches: [ "*" ]
# 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
permissions:
contents: read
contents: write
jobs:
build:
name: Packager
name: Build PluginLoader
runs-on: ubuntu-latest
steps:
- name: 🧰 Checkout
- name: Checkout 🧰
uses: actions/checkout@v3
- name: 💎 Set up NodeJS 17
- name: Set up NodeJS 17 💎
uses: actions/setup-node@v3
with:
node-version: 17
- name: 🐍 Set up Python 3.10
- name: Set up Python 3.10 🐍
uses: actions/setup-python@v3
with:
python-version: "3.10"
- name: ⬇️ Install Python dependencies
- name: Install Python dependencies ⬇️
run: |
python -m pip install --upgrade pip
pip install pyinstaller
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
[ -f requirements.txt ] && pip install -r requirements.txt
- name: ⬇️ Install NodeJS dependencies
- name: Install NodeJS dependencies ⬇️
run: |
cd frontend
npm i
npm run build
- name: 🛠️ Build
run: |
pyinstaller --noconfirm --onefile --name "Decky" --add-data ./backend/static:/static ./backend/*.py
- name: Build 🛠️
run: pyinstaller --noconfirm --onefile --name "PluginLoader" --add-data ./backend/static:/static --add-data ./backend/legacy:/legacy ./backend/*.py
- name: ⬆️ Upload package
uses: actions/upload-artifact@v2
- name: Upload package artifact ⬆️
uses: actions/upload-artifact@v3
with:
name: Plugin Loader
path: |
./dist/*
name: PluginLoader
path: ./dist/PluginLoader
release:
name: Release 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: Fetch package artifact ⬇️
uses: actions/download-artifact@v3
with:
name: PluginLoader
path: dist
- name: Bump version and push tag ⏫
id: tag_version
uses: mathieudutour/github-tag-action@v6.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Release 📦
uses: softprops/action-gh-release@v1
with:
name: Release ${{ steps.tag_version.outputs.new_tag }}
tag_name: ${{ steps.tag_version.outputs.new_tag }}
files: ./dist/PluginLoader
generate_release_notes: true
nightly:
name: Release the nightly version of the package
if: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.release == 'prerelease') }}
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout 🧰
uses: actions/checkout@v3
- name: Fetch package artifact ⬇️
uses: actions/download-artifact@v3
with:
name: PluginLoader
path: dist
- name: Get tag 🏷️
id: old_tag
uses: rafarlopes/get-latest-pre-release-tag-action@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
repository: 'decky-loader'
- name: Prepare tag ⚙️
id: ready_tag
run: |
export VERSION=${{ steps.old_tag.outputs.tag }}
export COMMIT=$(git log -1 --pretty=format:%h)
echo ::set-output name=tag_name::$(sed -r 's/(-.*)?-pre$//' <<< $VERSION)-$COMMIT-pre
- name: Push tag 📤
uses: rickstaa/action-create-tag@v1.3.2
if: ${{ steps.ready_tag.outputs.tag_name && github.event_name == 'workflow_dispatch' }}
with:
tag: ${{ steps.ready_tag.outputs.tag_name }}
message: Nightly ${{ steps.ready_tag.outputs.tag_name }}
- name: Release 📦
uses: softprops/action-gh-release@v1
if: ${{ github.event_name == 'workflow_dispatch' }}
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
# - name: Bump prerelease ⏫
# id: bump
# if: ${{ github.event_name == 'schedule' }}
# run: |
# git_hash=$(git rev-parse --short "$GITHUB_SHA")
# echo ::set-output new_tag="nightly-$git_hash"
# - name: Push tag 📤
# uses: rickstaa/action-create-tag@v1.3.2
# if: ${{ github.event_name == 'schedule' }}
# with:
# tag: ${{ steps.bump.outputs.new_tag }}
# message: Nightly ${{ steps.bump.outputs.new_tag }}
# - name: Release 📦
# uses: softprops/action-gh-release@v1
# if: ${{ github.event_name == 'schedule' }}
# with:
# name: Nightly ${{ steps.bump.outputs.new_tag }}
# tag_name: ${{ steps.bump.outputs.new_tag }}
# files: ./dist/PluginLoader
# prerelease: true
# generate_release_notes: true
+6 -2
View File
@@ -154,5 +154,9 @@ cython_debug/
# static files are built
backend/static
# pnpm lockfile
frontend/pnpm-lock.yaml
# ignore settings.json
# prevents leaking login details
.vscode/settings.json
# plugins folder for local launches
plugins/*
Vendored Executable
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" &> /dev/null && pwd 2> /dev/null; )";
# printf "${SCRIPT_DIR}\n"
# printf "$(dirname $0)\n"
if ! [[ -e "${SCRIPT_DIR}/settings.json" ]]; then
printf '.vscode/settings.json does not exist. Creating it with default settings. Exiting afterwards. Run your task again.\n\n'
cp "${SCRIPT_DIR}/defsettings.json" "${SCRIPT_DIR}/settings.json"
exit 1
else
printf '.vscode/settings.json does exist. Congrats.\n'
printf 'Make sure to change settings.json to match your deck.\n'
fi
+7
View File
@@ -0,0 +1,7 @@
{
"deckip" : "0.0.0.0",
"deckport" : "22",
"deckpass" : "ssap",
"deckkey" : "-i ${env:HOME}/.ssh/id_rsa",
"deckdir" : "/home/deck"
}
+12 -3
View File
@@ -2,16 +2,25 @@
"version": "0.2.0",
"configurations": [
{
"name": "Debug",
"name": "Run (Remote)",
"type": "python",
"request": "launch",
"console": "integratedTerminal",
"preLaunchTask": "remoterun",
"cwd": "",
"program": "",
},
{
"name": "Run (Local)",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/backend/main.py",
"cwd": "${workspaceFolder}/backend",
"console": "integratedTerminal",
"env": {
"PLUGIN_PATH": "/home/deck/homebrew/plugins"
"PLUGIN_PATH": "${workspaceFolder}/plugins"
},
"preLaunchTask": "Build frontend"
"preLaunchTask": "localrun"
}
]
}
+139 -5
View File
@@ -1,15 +1,149 @@
{
"version": "2.0.0",
"tasks": [
// OTHER
{
"label": "Stop Service",
"label": "checkforsettings",
"type": "shell",
"command":"systemctl --user stop plugin_loader",
"group": "none",
"detail": "Check that settings.json has been created",
"command": "bash -c ${workspaceFolder}/.vscode/config.sh",
"problemMatcher": []
},
{
"label": "Build frontend",
"label": "localrun",
"type": "shell",
"command":"cd ${workspaceFolder}/frontend; npm run build",
}
"group": "none",
"dependsOn" : ["buildall"],
"detail": "Check for local runs, create a plugins folder",
"command": "mkdir -p plugins",
"problemMatcher": []
},
{
"label": "remoterun",
"type": "shell",
"group": "none",
"dependsOn": [
"updateremote",
"runpydeck"
],
"detail": "Task for remote run launches",
"command": "exit 0",
"problemMatcher": []
},
{
"label": "dependencies",
"type": "shell",
"group": "none",
"detail": "Check for local runs, create a plugins folder",
"command": "rsync -azp --rsh='ssh -p ${config:deckport} ${config:deckkey}' requirements.txt deck@${config:deckip}:${config:deckdir}/homebrew/dev/pluginloader/requirements.txt && ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'python -m ensurepip && python -m pip install --upgrade pip && python -m pip install --upgrade setuptools && python -m pip install -r ${config:deckdir}/homebrew/dev/pluginloader/requirements.txt'",
"problemMatcher": []
},
// BUILD
{
"label": "pnpmsetup",
"type": "shell",
"group": "build",
"detail": "Setup pnpm",
"command": "cd frontend && pnpm i",
"problemMatcher": []
},
{
"label": "buildfrontend",
"type": "npm",
"group": "build",
"detail": "rollup -c",
"script": "build",
"path": "frontend",
"problemMatcher": [],
},
{
"label": "buildall",
"group": "build",
"detail": "Deploy pluginloader to deck",
"dependsOrder": "sequence",
"dependsOn": [
"pnpmsetup",
"buildfrontend"
],
"problemMatcher": []
},
// DEPLOY
{
"label": "createfolders",
"detail": "Create plugins folder in expected directory",
"type": "shell",
"group": "none",
"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'",
"problemMatcher": []
},
{
"label": "deploy",
"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='backend/__pycache__/' --exclude='.gitignore' . deck@${config:deckip}:${config:deckdir}/homebrew/dev/pluginloader",
"problemMatcher": []
},
{
"label": "deployall",
"dependsOrder": "sequence",
"group": "none",
"dependsOn": [
"createfolders",
"dependencies",
"deploy"
],
"problemMatcher": []
},
// RUN
{
"label": "runpydeck",
"detail": "Run indev PluginLoader on Deck",
"type": "shell",
"group": "none",
"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; echo '${config:deckpass}' | sudo -SE python3 ${config:deckdir}/homebrew/dev/pluginloader/backend/main.py'",
"problemMatcher": []
},
{
"label": "runpylocal",
"detail": "Run PluginLoader from python locally",
"type": "shell",
"group": "none",
"command": "export PLUGIN_PATH=${workspaceFolder}/plugins; export CHOWN_PLUGIN_PATH=0; sudo -E python3 ${workspaceFolder}/backend/main.py",
"problemMatcher": []
},
// ALL-IN-ONES
{
"label": "updateremote",
"detail": "Build and deploy",
"dependsOrder": "sequence",
"group": "none",
"dependsOn": [
"buildall",
"deployall",
],
"problemMatcher": []
},
{
"label": "allinone",
"detail": "Build, deploy and run",
"dependsOrder": "sequence",
"group": {
"kind": "build",
"isDefault": true
},
"dependsOn": [
"buildall",
"deployall",
"runpydeck"
],
"problemMatcher": []
}
]
}
+16 -33
View File
@@ -1,9 +1,3 @@
# TODO
- Fix button size/display
- Add plugin installation prompts for browser
- Fix components not updating unless tab opened first (with new tab hook)
- Clean up code
# Plugin Loader [![Chat](https://img.shields.io/badge/chat-on%20discord-7289da.svg)](https://discord.gg/ZU74G2NJzk)
![steamuserimages-a akamaihd](https://user-images.githubusercontent.com/10835354/161068262-ca723dc5-6795-417a-80f6-d8c1f9d03e93.jpg)
@@ -16,22 +10,25 @@ Keep an eye on the [Wiki](https://deckbrew.xyz) for more information about Plugi
3. Scroll the sidebar all the way down and click on `Developer`
4. Under Miscellaneous, enable `CEF Remote Debugging`
5. Click on the `STEAM` button and select `Power` -> `Switch to Desktop`
6. Open a terminal and paste the following command into it:
6. Make sure you have a password set with the "passwd" command in terminal to install it ([YouTube Guide](https://www.youtube.com/watch?v=1vOMYGj22rQ)).
7. Open a terminal and paste the following command into it:
- For users:
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/install_release.sh | sh`
- For plugin developers:
~~- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/install_nightly.sh | sh`~~
Nightly releases are currently broken.
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/legacy/dist/install_release.sh | sh`
- For the latest pre-release,
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/install_prerelease.sh | sh`
- For testers/plugin developers:
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/install_prerelease.sh | sh`
- [Wiki Link](https://deckbrew.xyz/en/loader-dev/development)
7. Done! Reboot back into Gaming mode and enjoy your plugins!
### Install Plugins
### Install/Uninstall Plugins
- Using the shopping bag button in the top right corner, you can go to the offical ["Plugin Store"](https://plugins.deckbrew.xyz/)
- Simply copy the plugin's folder into `~/homebrew/plugins`
- Use the settings menu to uninstall plugins, this will not remove any files made in different directories by plugins.
### Uninstall
- Open a terminal and paste the following command into it:
- For both users and developers:
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/uninstall.sh | sh`
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/uninstall.sh | sh`
## Features
- Clean injecting and loading of one or more plugins
@@ -43,25 +40,11 @@ Keep an eye on the [Wiki](https://deckbrew.xyz) for more information about Plugi
## Developing plugins
- There is no complete plugin development documentation yet. However a good starting point is the [Plugin Template](https://github.com/SteamDeckHomebrew/decky-plugin-template) repository.
## Contribution
- For Plugin Loader contributors (in possession of a Steam Deck):
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/react-frontend-plugins/contrib/deck.sh | sh`
- For PluginLoader contributors (without a Steam Deck):
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/react-frontend-plugins/contrib/pc.sh | sh`
- [Here's how to get the Steam Deck UI on your enviroment of choice.](https://youtu.be/1IAbZte8e7E?t=112)
- (The video shows Windows usage but unless you're using WSL/cygwin this script is unsupported on Windows.)
To run your development version of Plugin Loader on Deck, run a command like this:
```bash
ssh deck@steamdeck 'export PLUGIN_PATH=/home/deck/loaderdev/plugins; export CHOWN_PLUGIN_PATH=0; echo 'password' | sudo -SE python3 /home/deck/loaderdev/pluginloader/backend/main.py'
```
Or on PC with the Deck UI enabled:
```bash
export PLUGIN_PATH=/home/user/installdirectory/plugins;
export CHOWN_PLUGIN_PATH=0;
sudo python3 /home/deck/loaderdev/pluginloader/backend/main.py
```
## [Contribution](https://deckbrew.xyz/en/loader-dev/development)
- Please consult the [Wiki](https://deckbrew.xyz/en/loader-dev/development) for installing development versions of PluginLoader.
- This is also useful for Plugin Developers looking to target new but unreleased versions of PluginLoader.
- [Here's how to get the Steam Deck UI on your enviroment of choice.](https://youtu.be/1IAbZte8e7E?t=112)
- (The video shows Windows usage but unless you're using Arch WSL/cygwin this script is unsupported on Windows.)
Source control and deploying plugins are left to each respective contributor for the cloned repos in order to keep depedencies up to date.
+51 -27
View File
@@ -1,6 +1,6 @@
from injector import get_tab
from logging import getLogger
from os import path, rename
from os import path, rename, listdir
from shutil import rmtree
from aiohttp import ClientSession, web
from io import BytesIO
@@ -11,43 +11,70 @@ from time import time
from hashlib import sha256
from subprocess import Popen
import json
import helpers
class PluginInstallContext:
def __init__(self, gh_url, version, hash) -> None:
self.gh_url = gh_url
def __init__(self, artifact, name, version, hash) -> None:
self.artifact = artifact
self.name = name
self.version = version
self.hash = hash
class PluginBrowser:
def __init__(self, plugin_path, server_instance, store_url) -> None:
def __init__(self, plugin_path, server_instance) -> None:
self.log = getLogger("browser")
self.plugin_path = plugin_path
self.store_url = store_url
self.install_requests = {}
server_instance.add_routes([
web.post("/browser/install_plugin", self.install_plugin),
web.get("/browser/redirect", self.redirect_to_store)
web.post("/browser/uninstall_plugin", self.uninstall_plugin)
])
def _unzip_to_plugin_dir(self, zip, name, hash):
zip_hash = sha256(zip.getbuffer()).hexdigest()
if zip_hash != hash:
if hash and (zip_hash != hash):
return False
zip_file = ZipFile(zip)
zip_file.extractall(self.plugin_path)
rename(path.join(self.plugin_path, zip_file.namelist()[0]), path.join(self.plugin_path, name))
Popen(["chown", "-R", "deck:deck", self.plugin_path])
Popen(["chmod", "-R", "555", self.plugin_path])
return True
async def _install(self, artifact, version, hash):
name = artifact.split("/")[-1]
rmtree(path.join(self.plugin_path, name), ignore_errors=True)
self.log.info(f"Installing {artifact} (Version: {version})")
def find_plugin_folder(self, name):
for folder in listdir(self.plugin_path):
with open(path.join(self.plugin_path, folder, 'plugin.json'), 'r') as f:
plugin = json.load(f)
if plugin['name'] == name:
return path.join(self.plugin_path, folder)
async def uninstall_plugin(self, name):
tab = await get_tab("SP")
await tab.open_websocket()
try:
if type(name) != str:
data = await name.post()
name = data.get("name")
await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
rmtree(self.find_plugin_folder(name))
except FileNotFoundError:
self.log.warning(f"Plugin {name} not installed, skipping uninstallation")
return web.Response(text="Requested plugin uninstall")
async def _install(self, artifact, name, version, hash):
try:
await self.uninstall_plugin(name)
except:
self.log.error(f"Plugin {name} not installed, skipping uninstallation")
self.log.info(f"Installing {name} (Version: {version})")
async with ClientSession() as client:
url = f"https://github.com/{artifact}/archive/refs/tags/{version}.zip"
self.log.debug(f"Fetching {url}")
res = await client.get(url)
self.log.debug(f"Fetching {artifact}")
res = await client.get(artifact, ssl=helpers.get_ssl_context())
if res.status == 200:
self.log.debug("Got 200. Reading...")
data = await res.read()
@@ -63,30 +90,27 @@ class PluginBrowser:
hash
)
if ret:
self.log.info(f"Installed {artifact} (Version: {version})")
self.log.info(f"Installed {name} (Version: {version})")
else:
self.log.fatal(f"SHA-256 Mismatch!!!! {artifact} (Version: {version})")
self.log.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
else:
self.log.fatal(f"Could not fetch from github. {await res.text()}")
self.log.fatal(f"Could not fetch from URL. {await res.text()}")
async def redirect_to_store(self, request):
return web.Response(status=302, headers={"Location": self.store_url})
async def install_plugin(self, request):
data = await request.post()
get_event_loop().create_task(self.request_plugin_install(data["artifact"], data["version"], data["hash"]))
get_event_loop().create_task(self.request_plugin_install(data.get("artifact", ""), data.get("name", "No name"), data.get("version", "dev"), data.get("hash", False)))
return web.Response(text="Requested plugin install")
async def request_plugin_install(self, artifact, version, hash):
async def request_plugin_install(self, artifact, name, version, hash):
request_id = str(time())
self.install_requests[request_id] = PluginInstallContext(artifact, version, hash)
self.install_requests[request_id] = PluginInstallContext(artifact, name, version, hash)
tab = await get_tab("SP")
await tab.open_websocket()
await tab.evaluate_js(f"DeckyPluginLoader.addPluginInstallPrompt('{artifact}', '{version}', '{request_id}')")
await tab.evaluate_js(f"DeckyPluginLoader.addPluginInstallPrompt('{name}', '{version}', '{request_id}', '{hash}')")
async def confirm_plugin_install(self, request_id):
request = self.install_requests.pop(request_id)
await self._install(request.gh_url, request.version, request.hash)
await self._install(request.artifact, request.name, request.version, request.hash)
def cancel_plugin_install(self, request_id):
self.install_requests.pop(request_id)
self.install_requests.pop(request_id)
+7
View File
@@ -0,0 +1,7 @@
import ssl
import certifi
ssl_ctx = ssl.create_default_context(cafile=certifi.where())
def get_ssl_context():
return ssl_ctx
+8 -5
View File
@@ -33,8 +33,10 @@ class Tab:
return (await self.websocket.receive_json()) if receive else None
raise RuntimeError("Websocket not opened")
async def evaluate_js(self, js, run_async=False):
await self.open_websocket()
async def evaluate_js(self, js, run_async=False, manage_socket=True, get_result=True):
if manage_socket:
await self.open_websocket()
res = await self._send_devtools_cmd({
"id": 1,
"method": "Runtime.evaluate",
@@ -43,9 +45,10 @@ class Tab:
"userGesture": True,
"awaitPromise": run_async
}
})
}, get_result)
await self.client.close()
if manage_socket:
await self.client.close()
return res
async def get_steam_resource(self, url):
@@ -72,7 +75,7 @@ async def get_tabs():
r = await res.json()
return [Tab(i) for i in r]
else:
raise Exception(f"/json did not return 200. {await r.text()}")
raise Exception(f"/json did not return 200. {await res.text()}")
async def get_tab(tab_name):
tabs = await get_tabs()
+78
View File
@@ -0,0 +1,78 @@
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 response = await fetch(`http://127.0.0.1:1337/methods/${method_name}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
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 response = await fetch(`http://127.0.0.1:1337/plugins/${plugin_name}/methods/${method_name}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
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
});
}
+3 -3
View File
@@ -116,7 +116,7 @@ class Loader:
self.logger.info(f"Plugin {plugin.name} is passive")
self.plugins[plugin.name] = plugin.start()
self.logger.info(f"Loaded {plugin.name}")
self.loop.create_task(self.dispatch_plugin(plugin.name))
self.loop.create_task(self.dispatch_plugin(plugin.name if not plugin.legacy else "$LEGACY_" + plugin.name))
except Exception as e:
self.logger.error(f"Could not load {file}. {e}")
print_exc()
@@ -168,8 +168,8 @@ class Loader:
with open(path.join(self.plugin_path, plugin.plugin_directory, plugin.main_view_html), 'r') as template:
template_data = template.read()
ret = f"""
<script src="/static/legacy-library.js"></script>
<script>const plugin_name = '{plugin.name}' </script>
<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}
"""
+10 -6
View File
@@ -9,8 +9,7 @@ CONFIG = {
"server_host": getenv("SERVER_HOST", "127.0.0.1"),
"server_port": int(getenv("SERVER_PORT", "1337")),
"live_reload": getenv("LIVE_RELOAD", "1") == "1",
"log_level": {"CRITICAL": 50, "ERROR": 40, "WARNING":30, "INFO": 20, "DEBUG": 10}[getenv("LOG_LEVEL", "INFO")],
"store_url": getenv("STORE_URL", "https://beta.deckbrew.xyz")
"log_level": {"CRITICAL": 50, "ERROR": 40, "WARNING":30, "INFO": 20, "DEBUG": 10}[getenv("LOG_LEVEL", "INFO")]
}
basicConfig(level=CONFIG["log_level"], format="[%(module)s][%(levelname)s]: %(message)s")
@@ -18,7 +17,7 @@ basicConfig(level=CONFIG["log_level"], format="[%(module)s][%(levelname)s]: %(me
from asyncio import get_event_loop, sleep
from json import dumps, loads
from os import path
from subprocess import Popen
from subprocess import call
import aiohttp_cors
from aiohttp.web import Application, run_app, static
@@ -28,12 +27,15 @@ from browser import PluginBrowser
from injector import inject_to_tab, tab_has_global_var
from loader import Loader
from utilities import Utilities
from updater import Updater
logger = getLogger("Main")
async def chown_plugin_dir(_):
Popen(["chown", "-R", "deck:deck", CONFIG["plugin_path"]])
Popen(["chmod", "-R", "555", CONFIG["plugin_path"]])
code_chown = call(["chown", "-R", "deck:deck", CONFIG["plugin_path"]])
code_chmod = call(["chmod", "-R", "555", CONFIG["plugin_path"]])
if code_chown != 0 or code_chmod != 0:
logger.error(f"chown/chmod exited with a non-zero exit code (chown: {code_chown}, chmod: {code_chmod})")
class PluginManager:
def __init__(self) -> None:
@@ -44,8 +46,9 @@ class PluginManager:
allow_headers="*")
})
self.plugin_loader = Loader(self.web_app, CONFIG["plugin_path"], self.loop, CONFIG["live_reload"])
self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.web_app, CONFIG["store_url"])
self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.web_app)
self.utilities = Utilities(self)
self.updater = Updater(self)
jinja_setup(self.web_app)
self.web_app.on_startup.append(self.inject_javascript)
@@ -57,6 +60,7 @@ class PluginManager:
for route in list(self.web_app.router.routes()):
self.cors.add(route)
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, context):
if context["message"] == "Unclosed connection":
+20 -15
View File
@@ -78,12 +78,17 @@ class PluginWrapper:
async def _open_socket_if_not_exists(self):
if not self.reader:
while True:
retries = 0
while retries < 10:
try:
self.reader, self.writer = await open_unix_connection(self.socket_addr)
break
return True
except:
await sleep(0)
await sleep(2)
retries += 1
return False
else:
return True
def start(self):
if self.passive:
@@ -95,21 +100,21 @@ class PluginWrapper:
if self.passive:
return
async def _(self):
await self._open_socket_if_not_exists()
self.writer.write((dumps({"stop": True})+"\n").encode("utf-8"))
await self.writer.drain()
self.writer.close()
if await self._open_socket_if_not_exists():
self.writer.write((dumps({"stop": True})+"\n").encode("utf-8"))
await self.writer.drain()
self.writer.close()
get_event_loop().create_task(_(self))
async def execute_method(self, method_name, kwargs):
if self.passive:
raise RuntimeError("This plugin is passive (aka does not implement main.py)")
async with self.method_call_lock:
await self._open_socket_if_not_exists()
self.writer.write(
(dumps({"method": method_name, "args": kwargs})+"\n").encode("utf-8"))
await self.writer.drain()
res = loads((await self.reader.readline()).decode("utf-8"))
if not res["success"]:
raise Exception(res["res"])
return res["res"]
if await self._open_socket_if_not_exists():
self.writer.write(
(dumps({"method": method_name, "args": kwargs})+"\n").encode("utf-8"))
await self.writer.drain()
res = loads((await self.reader.readline()).decode("utf-8"))
if not res["success"]:
raise Exception(res["res"])
return res["res"]
+120
View File
@@ -0,0 +1,120 @@
import uuid
from logging import getLogger
from json.decoder import JSONDecodeError
from asyncio import sleep
from aiohttp import ClientSession, web
from injector import inject_to_tab, get_tab
from os import getcwd, path, remove
from subprocess import call
import helpers
logger = getLogger("Updater")
class Updater:
def __init__(self, context) -> None:
self.context = context
self.updater_methods = {
"get_version": self.get_version,
"do_update": self.do_update,
"do_restart": self.do_restart,
"check_for_updates": self.check_for_updates
}
self.remoteVer = None
try:
with open(path.join(getcwd(), ".loader.version"), 'r') as version_file:
self.localVer = version_file.readline().replace("\n", "")
except:
self.localVer = False
if context:
context.web_app.add_routes([
web.post("/updater/{method_name}", self._handle_server_method_call)
])
context.loop.create_task(self.version_reloader())
async def _handle_server_method_call(self, 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)
res["result"] = r
res["success"] = True
except Exception as e:
res["result"] = str(e)
res["success"] = False
return web.json_response(res)
async def get_version(self):
if self.localVer:
return {
"current": self.localVer,
"remote": self.remoteVer,
"updatable": self.localVer != None
}
else:
return {"current": "unknown", "updatable": False}
async def check_for_updates(self):
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:
remoteVersions = await res.json()
self.remoteVer = next(filter(lambda ver: ver["prerelease"] and ver["tag_name"].startswith("v") and ver["tag_name"].endswith("-pre"), remoteVersions), None)
logger.info("Updated remote version information")
return await self.get_version()
async def version_reloader(self):
await sleep(30)
while True:
try:
await self.check_for_updates()
except:
pass
await sleep(60 * 60) # 1 hour
async def do_update(self):
version = self.remoteVer["tag_name"]
#TODO don't hardcode this
download_url = self.remoteVer["assets"][0]["browser_download_url"]
tab = await get_tab("SP")
await tab.open_websocket()
async with ClientSession() as web:
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))
try:
remove(path.join(getcwd(), "PluginLoader"))
except:
pass
with open(path.join(getcwd(), "PluginLoader"), "wb") as out:
progress = 0
raw = 0
async for c in res.content.iter_chunked(512):
out.write(c)
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
with open(path.join(getcwd(), ".loader.version"), "w") as out:
out.write(version)
call(['chmod', '+x', path.join(getcwd(), "PluginLoader")])
logger.info("Updated loader installation.")
await tab.evaluate_js("window.DeckyUpdater.finish()", False, False)
await tab.client.close()
async def do_restart(self):
call(["systemctl", "daemon-reload"])
call(["systemctl", "restart", "plugin_loader"])
+2 -2
View File
@@ -4,7 +4,7 @@ from json.decoder import JSONDecodeError
from aiohttp import ClientSession, web
from injector import inject_to_tab
import helpers
class Utilities:
def __init__(self, context) -> None:
@@ -48,7 +48,7 @@ class Utilities:
async def http_request(self, method="", url="", **kwargs):
async with ClientSession() as web:
async with web.request(method, url, **kwargs) as res:
async with web.request(method, url, ssl=helpers.get_ssl_context(), **kwargs) as res:
return {
"status": res.status,
"headers": dict(res.headers),
+105 -52
View File
@@ -4,6 +4,8 @@
## This script defaults to port 22 unless otherwise specified, and cannot run without a sudo password or LAN IP.
## You will need to specify the path to the ssh key if using key connection exclusively.
## TODO: document latest changes to wiki
## Pre-parse arugments for ease of use
CLONEFOLDER=${1:-""}
INSTALLFOLDER=${2:-""}
@@ -11,9 +13,13 @@ DECKIP=${3:-""}
SSHPORT=${4:-""}
PASSWORD=${5:-""}
SSHKEYLOC=${6:-""}
LOADERBRANCH=${7:-""}
LIBRARYBRANCH=${8:-""}
TEMPLATEBRANCH=${9:-""}
LATEST=${10:-""}
## gather options into an array
OPTIONSARRAY=("$CLONEFOLDER" $INSTALLFOLDER "$DECKIP" "$SSHPORT" "$PASSWORD" "$SSHKEYLOC")
OPTIONSARRAY=("$CLONEFOLDER" "$INSTALLFOLDER" "$DECKIP" "$SSHPORT" "$PASSWORD" "$SSHKEYLOC" "$LOADERBRANCH" "$LIBRARYBRANCH" "$TEMPLATEBRANCH" "$LATEST")
## iterate through options array to check their presence
count=0
@@ -28,19 +34,21 @@ setfolder() {
local DEFAULT="git"
elif [[ "$2" == "install" ]]; then
local ACTION="install"
local DEFAULT="loaderdev"
local DEFAULT="dev"
fi
printf "Enter the directory in /home/user to ${ACTION} to.\n"
printf "Example: if your home directory is /home/user you would type: ${DEFAULT}\n"
printf "The ${ACTION} directory would be: ${HOME}/${DEFAULT}\n"
if [[ "$ACTION" == "clone" ]]; then
printf "Enter the directory in /home/user/ to ${ACTION} to.\n"
printf "The ${ACTION} directory would be: ${HOME}/${DEFAULT}\n"
read -p "Enter your ${ACTION} directory: " CLONEFOLDER
if ! [[ "$CLONEFOLDER" =~ ^[[:alnum:]]+$ ]]; then
printf "Folder name not provided. Using default, '${DEFAULT}'.\n"
CLONEFOLDER="${DEFAULT}"
fi
elif [[ "$ACTION" == "install" ]]; then
printf "Enter the directory in /home/deck/homebrew to ${ACTION} pluginloader to.\n"
printf "The ${ACTION} directory would be: /home/deck/homebrew/${DEFAULT}/pluginloader\n"
printf "It is highly recommended that you use the default folder path seen above, just press enter at the next prompt.\n"
read -p "Enter your ${ACTION} directory: " INSTALLFOLDER
if ! [[ "$INSTALLFOLDER" =~ ^[[:alnum:]]+$ ]]; then
printf "Folder name not provided. Using default, '${DEFAULT}'.\n"
@@ -106,47 +114,81 @@ clonefromto() {
# printf "repo=$1\n"
# printf "outdir=$2\n"
# printf "branch=$3\n"
if [[ -z $3 ]]; then
BRANCH=""
else
BRANCH="-b $3"
fi
git clone $1 $2 $BRANCH &> '/dev/null'
printf "Repository: $1\n"
git clone $1 $2 &> '/dev/null'
CODE=$?
# printf "CODE=${CODE}"
if [[ $CODE -eq 128 ]]; then
cd $2
git fetch &> '/dev/null'
git fetch --all &> '/dev/null'
fi
if [[ -z $3 ]]; then
printf "Enter the desired branch for repository "$1" :\n"
local OUT="$(git branch -r | sed '/\/HEAD/d')"
# $OUT="$($OUT > )"
printf "$OUT\nbranch: "
read BRANCH
else
printf "on branch: $3\n"
BRANCH="$3"
fi
if ! [[ -z ${BRANCH} ]]; then
git checkout $BRANCH &> '/dev/null'
fi
if [[ ${LATEST} == "true" ]]; then
git pull --all
elif [[ ${LATEST} == "true" ]]; then
printf "Assuming user not pulling latest commits.\n"
else
printf "Pull latest commits? (y/N): "
read PULL
case ${PULL:0:1} in
y|Y )
printf "Pulling latest commits.\n"
git pull --all
;;
* )
printf "Not pulling latest commits.\n"
;;
esac
if ! [[ "$PULL" =~ ^[[:alnum:]]+$ ]]; then
printf "Assuming user not pulling latest commits.\n"
fi
fi
}
npmtransbundle() {
pnpmtransbundle() {
cd $1
if [[ "$2" == "library" ]]; then
npm install --quiet &> '/dev/null'
npm run build --quiet &> '/dev/null'
sudo npm link --quiet &> '/dev/null'
elif [[ "$2" == "frontend" ]] || [[ "$2" == "template" ]]; then
npm install --quiet &> '/dev/null'
npm link decky-frontend-lib --quiet &> '/dev/null'
npm run build --quiet &> '/dev/null'
elif [[ "$2" == "frontend" ]]; then
pnpm i &> '/dev/null'
pnpm run build &> '/dev/null'
elif [[ "$2" == "template" ]]; then
pnpm i &> '/dev/null'
pnpm run build &> '/dev/null'
fi
}
printf "Installing Steam Deck Plugin Loader contributor (for Steam Deck)...\n"
if ! [[ $count -gt 9 ]] ; then
printf "Installing Steam Deck Plugin Loader contributor/developer (for Steam Deck)...\n"
printf "THIS SCRIPT ASSUMES YOU ARE RUNNING IT ON A PC, NOT THE DECK!
Not planning to contribute to PluginLoader?
If so, you should not be using this script.\n
If you have a release/nightly installed this script will disable it.\n"
printf "THIS SCRIPT ASSUMES YOU ARE RUNNING IT ON A PC, NOT THE DECK!
Not planning to contribute to or develop for PluginLoader?
If so, you should not be using this script.\n
If you have a release/nightly installed this script will disable it.\n"
printf "This script requires you to have nodejs installed. (If nodejs doesn't bundle npm on your OS/distro, then npm is required as well).\n"
# [[ $count -gt 0 ]] || read -p "Press any key to continue"
printf "This script requires you to have nodejs installed. (If nodejs doesn't bundle npm on your OS/distro, then npm is required as well).\n"
fi
if ! [[ $count -gt 0 ]] ; then
read -p "Press any key to continue"
fi
printf "\n"
## User chooses preffered clone & install directories
if [[ "$CLONEFOLDER" == "" ]]; then
@@ -158,7 +200,7 @@ if [[ "$INSTALLFOLDER" == "" ]]; then
fi
CLONEDIR="$HOME/$CLONEFOLDER"
INSTALLDIR="/home/deck/$INSTALLFOLDER"
INSTALLDIR="/home/deck/homebrew/$INSTALLFOLDER"
## Input ip address, port, password and sshkey
@@ -208,7 +250,7 @@ fi
## Create folder structure
printf "\nCloning git repositories.\n"
printf "Cloning git repositories.\n"
mkdir -p ${CLONEDIR} &> '/dev/null'
@@ -217,61 +259,72 @@ mkdir -p ${CLONEDIR} &> '/dev/null'
# rm -r ${CLONEDIR}/pluginlibrary
# rm -r ${CLONEDIR}/plugintemplate
clonefromto "https://github.com/SteamDeckHomebrew/PluginLoader" ${CLONEDIR}/pluginloader react-frontend-plugins
clonefromto "https://github.com/SteamDeckHomebrew/PluginLoader" ${CLONEDIR}/pluginloader "$LOADERBRANCH"
clonefromto "https://github.com/SteamDeckHomebrew/decky-frontend-lib" ${CLONEDIR}/pluginlibrary
clonefromto "https://github.com/SteamDeckHomebrew/decky-frontend-lib" ${CLONEDIR}/pluginlibrary "$LIBRARYBRANCH"
clonefromto "https://github.com/SteamDeckHomebrew/decky-plugin-template" ${CLONEDIR}/plugintemplate
clonefromto "https://github.com/SteamDeckHomebrew/decky-plugin-template" ${CLONEDIR}/plugintemplate "$TEMPLATEBRANCH"
## install python dependencies to deck
printf "\nInstalling python dependencies.\n"
rsync -azp --rsh="ssh -p $SSHPORT $IDENINVOC" ${CLONEDIR}/pluginloader/requirements.txt deck@${DECKIP}:${INSTALLDIR}/pluginloader/requirements.txt &> '/dev/null'
ssh deck@${DECKIP} -p ${SSHPORT} ${IDENINVOC} "python -m ensurepip && python -m pip install --upgrade pip && python -m pip install --upgrade setuptools && python -m pip install -r $INSTALLDIR/pluginloader/requirements.txt" &> '/dev/null'
## Transpile and bundle typescript
type npm &> '/dev/null'
[ "$UID" -eq 0 ] || printf "Input password to proceed with install.\n"
NPMLIVES=$?
sudo npm install -g pnpm &> '/dev/null'
if ! [[ "$NPMLIVES" -eq 0 ]]; then
printf "npm does not to be installed, exiting.\n"
type pnpm &> '/dev/null'
PNPMLIVES=$?
if ! [[ "$PNPMLIVES" -eq 0 ]]; then
printf "pnpm does not appear to be installed, exiting.\n"
exit 1
fi
[ "$UID" -eq 0 ] || printf "Input password to install typscript compiler.\n"
## TODO: add a way of verifying if tsc is installed and to skip this step if it is
sudo npm install --quiet -g tsc &> '/dev/null'
printf "Transpiling and bundling typescript.\n"
npmtransbundle ${CLONEDIR}/pluginlibrary/ "library"
pnpmtransbundle ${CLONEDIR}/pluginlibrary/ "library"
npmtransbundle ${CLONEDIR}/pluginloader/frontend "frontend"
pnpmtransbundle ${CLONEDIR}/pluginloader/frontend "frontend"
npmtransbundle ${CLONEDIR}/plugintemplate "template"
pnpmtransbundle ${CLONEDIR}/plugintemplate "template"
## Transfer relevant files to deck
printf "Copying relevant files to install directory\n\n"
ssh deck@${DECKIP} -p ${SSHPORT} ${IDENINVOC} "mkdir -p $INSTALLDIR/pluginloader && mkdir -p $INSTALLDIR/plugins" &> '/dev/null'
### copy files for PluginLoader
rsync -avzp --mkpath --rsh="ssh -p ${SSHPORT} ${IDENINVOC}" --exclude='.git/' --exclude='node_modules' --exclude="package-lock.json" --exclude=='frontend' --exclude="*dist*" --exclude="*contrib*" --delete ${CLONEDIR}/pluginloader/* deck@${DECKIP}:${INSTALLDIR}/pluginloader/ &> '/dev/null'
rsync -avzp --rsh="ssh -p $SSHPORT $IDENINVOC" --exclude='.git/' --exclude='.github/' --exclude='.vscode/' --exclude='frontend/' --exclude='dist/' --exclude='contrib/' --exclude='*.log' --exclude='requirements.txt' --exclude='backend/__pycache__/' --exclude='.gitignore' --delete ${CLONEDIR}/pluginloader/* deck@${DECKIP}:${INSTALLDIR}/pluginloader &> '/dev/null'
if ! [[ $? -eq 0 ]]; then
printf "Error occurred when copying ${CLONEDIR}/pluginloader/ to ${INSTALLDIR}/pluginloader/\n"
printf "Error occurred when copying $CLONEDIR/pluginloader/ to $INSTALLDIR/pluginloader/\n"
printf "Check that your Steam Deck is active, ssh is enabled and running and is accepting connections.\n"
exit 1
fi
### copy files for PluginLoader template
rsync -avzp --mkpath --rsh="ssh -p ${SSHPORT} ${IDENINVOC}" --exclude='.git/' --exclude='node_modules' --exclude="package-lock.json" --delete ${CLONEDIR}/plugintemplate deck@${DECKIP}:${INSTALLDIR}/plugins &> '/dev/null'
### copy files for plugin template
rsync -avzp --rsh="ssh -p $SSHPORT $IDENINVOC" --exclude='.git/' --exclude='.github/' --exclude='.vscode/' --exclude='node_modules/' --exclude='src/' --exclude='*.log' --exclude='.gitignore' --exclude='pnpm-lock.yaml' --exclude='package.json' --exclude='rollup.config.js' --exclude='tsconfig.json' --delete ${CLONEDIR}/plugintemplate deck@${DECKIP}:${INSTALLDIR}/plugins &> '/dev/null'
if ! [[ $? -eq 0 ]]; then
printf "Error occurred when copying ${CLONEDIR}/plugintemplate to ${INSTALLDIR}/plugins\n"
printf "Error occurred when copying $CLONEDIR/plugintemplate to $INSTALLDIR/plugins\n"
exit 1
fi
## TODO: direct contributors to wiki for this info?
printf "Run these commands to deploy your local changes to the deck:\n"
printf "'rsync -avzp --mkpath --rsh=""\"ssh -p ${SSHPORT} ${IDENINVOC}\""" --exclude='.git/' --exclude='node_modules' --exclude='package-lock.json' --delete ${CLONEDIR}/pluginname deck@${DECKIP}:${INSTALLDIR}/plugins'\n"
printf "'rsync -avzp --mkpath --rsh=""\"ssh -p ${SSHPORT} ${IDENINVOC}\""" --exclude='.git/' --exclude='node_modules' --exclude='package-lock.json' --exclude=='frontend' --exclude='*dist*' --exclude='*contrib*' --delete ${CLONEDIR}/pluginloader/* deck@${DECKIP}:${INSTALLDIR}/pluginloader/'\n"
printf "'rsync -avzp --mkpath --rsh=""\"ssh -p $SSHPORT $IDENINVOC\""" --exclude='.git/' --exclude='.github/' --exclude='.vscode/' --exclude='frontend/' --exclude='dist/' --exclude='contrib/' --exclude='*.log' --exclude='requirements.txt' --exclude='backend/__pycache__/' --exclude='.gitignore' --delete $CLONEDIR/pluginloader/* deck@$DECKIP:$INSTALLDIR/pluginloader/'\n"
printf "'rsync -avzp --mkpath --rsh=""\"ssh -p $SSHPORT $IDENINVOC\""" --exclude='.git/' --exclude='.github/' --exclude='.vscode/' --exclude='node_modules/' --exclude='src/' --exclude='*.log' --exclude='.gitignore' --exclude='package-lock.json' --delete $CLONEDIR/pluginname deck@$DECKIP:$INSTALLDIR/plugins'\n\n"
printf "Run in console or in a script this command to run your development version:\n'ssh deck@${DECKIP} -p 22 ${IDENINVOC} 'export PLUGIN_PATH=${INSTALLDIR}/plugins; export CHOWN_PLUGIN_PATH=0; echo 'steam' | sudo -SE python3 ${INSTALLDIR}/pluginloader/backend/main.py'\n"
printf "Run in console or in a script this command to run your development version:\n'ssh deck@$DECKIP -p $SSHPORT $IDENINVOC 'export PLUGIN_PATH=$INSTALLDIR/plugins; export CHOWN_PLUGIN_PATH=0; echo 'steam' | sudo -SE python3 $INSTALLDIR/pluginloader/backend/main.py'\n"
## Disable Releases versions if they exist
@@ -279,4 +332,4 @@ printf "Run in console or in a script this command to run your development versi
printf "Connecting via ssh to disable any PluginLoader release versions.\n"
printf "Script will exit after this. All done!\n"
ssh deck@$DECKIP -p $SSHPORT $IDENINVOC "printf ${PASSWORD} | sudo -S systemctl disable --now plugin_loader; echo $?"
ssh deck@${DECKIP} -p ${SSHPORT} ${IDENINVOC} "printf $PASSWORD | sudo -S systemctl disable --now plugin_loader; echo $?" &> '/dev/null'
+99 -57
View File
@@ -2,87 +2,115 @@
## Pre-parse arugments for ease of use
CLONEFOLDER=${1:-""}
LOADERBRANCH=${2:-""}
LIBRARYBRANCH=${3:-""}
TEMPLATEBRANCH=${4:-""}
LATEST=${5:-""}
setfolder() {
if [[ "$2" == "clone" ]]; then
local ACTION="clone"
local DEFAULT="git"
elif [[ "$2" == "install" ]]; then
local ACTION="install"
local DEFAULT="loaderdev"
fi
## gather options into an array
OPTIONSARRAY=("$CLONEFOLDER" "$LOADERBRANCH" "$LIBRARYBRANCH" "$TEMPLATEBRANCH" "$LATEST")
printf "Enter the directory in /home/user to ${ACTION} to.\n"
printf "Example: if your home directory is /home/user you would type: ${DEFAULT}\n"
printf "The ${ACTION} directory would be: ${HOME}/${DEFAULT}\n"
if [[ "$ACTION" == "clone" ]]; then
read -p "Enter your ${ACTION} directory: " CLONEFOLDER
if ! [[ "$CLONEFOLDER" =~ ^[[:alnum:]]+$ ]]; then
printf "Folder name not provided. Using default, '${DEFAULT}'.\n"
CLONEFOLDER="${DEFAULT}"
fi
elif [[ "$ACTION" == "install" ]]; then
read -p "Enter your ${ACTION} directory: " INSTALLFOLDER
if ! [[ "$INSTALLFOLDER" =~ ^[[:alnum:]]+$ ]]; then
printf "Folder name not provided. Using default, '${DEFAULT}'.\n"
INSTALLFOLDER="${DEFAULT}"
fi
else
printf "Folder type could not be determined, exiting\n"
exit 1
fi
}
## iterate through options array to check their presence
count=0
for OPTION in ${OPTIONSARRAY[@]}; do
! [[ "$OPTION" == "" ]] && count=$(($count+1))
# printf "OPTION=$OPTION\n"
done
clonefromto() {
# printf "repo=$1\n"
# printf "outdir=$2\n"
# printf "branch=$3\n"
if [[ -z $3 ]]; then
BRANCH=""
else
BRANCH="-b $3"
fi
git clone $1 $2 $BRANCH &> '/dev/null'
printf "Repository: $1\n"
git clone $1 $2 &> '/dev/null'
CODE=$?
# printf "CODE=${CODE}"
if [[ $CODE -eq 128 ]]; then
cd $2
git fetch &> '/dev/null'
git fetch --all &> '/dev/null'
fi
if [[ -z $3 ]]; then
printf "Enter the desired branch for repository "$1" :\n"
local OUT="$(git branch -r | sed '/\/HEAD/d')"
# $OUT="$($OUT > )"
printf "$OUT\nbranch: "
read BRANCH
else
printf "on branch: $3\n"
BRANCH="$3"
fi
if ! [[ -z ${BRANCH} ]]; then
git checkout $BRANCH &> '/dev/null'
fi
if [[ ${LATEST} == "true" ]]; then
git pull --all
elif [[ ${LATEST} == "true" ]]; then
printf "Assuming user not pulling latest commits.\n"
else
printf "Pull latest commits? (y/N): "
read PULL
case ${PULL:0:1} in
y|Y )
printf "Pulling latest commits.\n"
git pull --all
;;
* )
printf "Not pulling latest commits.\n"
;;
esac
if ! [[ "$PULL" =~ ^[[:alnum:]]+$ ]]; then
printf "Assuming user not pulling latest commits.\n"
fi
fi
}
npmtransbundle() {
pnpmtransbundle() {
cd $1
if [[ "$2" == "library" ]]; then
npm install --quiet &> '/dev/null'
npm run build --quiet &> '/dev/null'
sudo npm link --quiet &> '/dev/null'
elif [[ "$2" == "frontend" ]] || [[ "$2" == "template" ]]; then
npm install --quiet &> '/dev/null'
npm link decky-frontend-lib --quiet &> '/dev/null'
npm run build --quiet &> '/dev/null'
elif [[ "$2" == "frontend" ]]; then
pnpm i &> '/dev/null'
pnpm run build &> '/dev/null'
elif [[ "$2" == "template" ]]; then
pnpm i &> '/dev/null'
pnpm run build &> '/dev/null'
fi
}
printf "Installing Steam Deck Plugin Loader contributor (no Steam Deck)..."
printf "\nTHIS SCRIPT ASSUMES YOU ARE RUNNING IT ON A PC, NOT THE DECK!
If you are not planning to contribute to PluginLoader then you should not be using this script.\n"
if ! [[ $count -gt 4 ]] ; then
printf "Installing Steam Deck Plugin Loader contributor/developer (no Steam Deck)..."
printf "\nThis script requires you to have nodejs installed. (If nodejs doesn't bundle npm on your OS/distro, then npm is required as well).\n"
printf "\nTHIS SCRIPT ASSUMES YOU ARE RUNNING IT ON A PC, NOT THE DECK!
Not planning to contribute to or develop for PluginLoader?
Then you should not be using this script.\n"
if [[ -z $1 ]]; then
printf "\nThis script requires you to have nodejs installed. (If nodejs doesn't bundle npm on your OS/distro, then npm is required as well).\n"
fi
if ! [[ $count -gt 0 ]] ; then
read -p "Press any key to continue"
fi
printf "\n"
if [[ "$CLONEFOLDER" == "" ]]; then
setfolder "$CLONEFOLDER" "clone"
printf "Enter the directory in /home/user/ to clone to.\n"
printf "The clone directory would be: ${HOME}/git \n"
read -p "Enter your clone directory: " CLONEFOLDER
if ! [[ "$CLONEFOLDER" =~ ^[[:alnum:]]+$ ]]; then
printf "Folder name not provided. Using default, '${DEFAULT}'.\n"
CLONEFOLDER="${DEFAULT}"
fi
fi
CLONEDIR="$HOME/$CLONEFOLDER"
## Create folder structure
printf "\nCloning git repositories.\n"
printf "Cloning git repositories.\n"
mkdir -p ${CLONEDIR} &> '/dev/null'
@@ -91,33 +119,47 @@ mkdir -p ${CLONEDIR} &> '/dev/null'
# rm -r ${CLONEDIR}/pluginlibrary
# rm -r ${CLONEDIR}/plugintemplate
clonefromto "https://github.com/SteamDeckHomebrew/PluginLoader" ${CLONEDIR}/pluginloader react-frontend-plugins
clonefromto "https://github.com/SteamDeckHomebrew/PluginLoader" ${CLONEDIR}/pluginloader "$LOADERBRANCH"
clonefromto "https://github.com/SteamDeckHomebrew/decky-frontend-lib" ${CLONEDIR}/pluginlibrary
clonefromto "https://github.com/SteamDeckHomebrew/decky-frontend-lib" ${CLONEDIR}/pluginlibrary "$LIBRARYBRANCH"
clonefromto "https://github.com/SteamDeckHomebrew/decky-plugin-template" ${CLONEDIR}/plugintemplate
clonefromto "https://github.com/SteamDeckHomebrew/decky-plugin-template" ${CLONEDIR}/plugintemplate "$TEMPLATEBRANCH"
## install python dependencies (maybe use venv?)
python -m pip install -r ${CLONEDIR}/pluginloader/requirements.txt &> '/dev/null'
## Transpile and bundle typescript
[ "$UID" -eq 0 ] || printf "Input password to proceed with install.\n"
type npm &> '/dev/null'
NPMLIVES=$?
if ! [[ "$NPMLIVES" -eq 0 ]]; then
printf "npm needs to be installed, exiting.\n"
if ! [[ "$PNPMLIVES" -eq 0 ]]; then
printf "npm does not appear to be installed, exiting.\n"
exit 1
fi
[ "$UID" -eq 0 ] || printf "Input password to install typscript compiler.\n"
sudo npm install -g pnpm &> '/dev/null'
sudo npm install --quiet -g tsc &> '/dev/null'
type pnpm &> '/dev/null'
PNPMLIVES=$?
if ! [[ "$PNPMLIVES" -eq 0 ]]; then
printf "pnpm does not appear to be installed, exiting.\n"
exit 1
fi
printf "Transpiling and bundling typescript.\n"
npmtransbundle ${CLONEDIR}/pluginlibrary/ "library"
pnpmtransbundle ${CLONEDIR}/pluginlibrary/ "library"
npmtransbundle ${CLONEDIR}/pluginloader/frontend "frontend"
pnpmtransbundle ${CLONEDIR}/pluginloader/frontend "frontend"
npmtransbundle ${CLONEDIR}/plugintemplate "template"
pnpmtransbundle ${CLONEDIR}/plugintemplate "template"
printf "Plugin Loader is located at '${CLONEDIR}/pluginloader/'.\n"
Vendored Executable
+45
View File
@@ -0,0 +1,45 @@
#!/bin/sh
[ "$UID" -eq 0 ] || exec sudo "$0" "$@"
echo "Installing Steam Deck Plugin Loader pre-release..."
HOMEBREW_FOLDER=/home/deck/homebrew
# # Create folder structure
rm -rf ${HOMEBREW_FOLDER}/services
sudo -u deck mkdir -p ${HOMEBREW_FOLDER}/services
sudo -u deck mkdir -p ${HOMEBREW_FOLDER}/plugins
# Download latest release and install it
RELEASE=$(curl -s 'https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases' | jq -r "first(.[] | select(.prerelease == "true"))")
read VERSION DOWNLOADURL < <(echo $(jq -r '.tag_name, .assets[].browser_download_url' <<< ${RELEASE}))
printf "Installing version %s...\n" "${VERSION}"
curl -L $DOWNLOADURL --output ${HOMEBREW_FOLDER}/services/PluginLoader
chmod +x ${HOMEBREW_FOLDER}/services/PluginLoader
echo $VERSION > ${HOMEBREW_FOLDER}/services/.loader.version
systemctl --user stop plugin_loader 2> /dev/null
systemctl --user disable plugin_loader 2> /dev/null
systemctl stop plugin_loader 2> /dev/null
systemctl disable plugin_loader 2> /dev/null
rm -f /etc/systemd/system/plugin_loader.service
cat > /etc/systemd/system/plugin_loader.service <<- EOM
[Unit]
Description=SteamDeck Plugin Loader
[Service]
Type=simple
User=root
Restart=always
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
WorkingDirectory=${HOMEBREW_FOLDER}/services
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
Environment=LOG_LEVEL=DEBUG
[Install]
WantedBy=multi-user.target
EOM
systemctl daemon-reload
systemctl start plugin_loader
systemctl enable plugin_loader
-3881
View File
File diff suppressed because it is too large Load Diff
+8 -7
View File
@@ -13,21 +13,22 @@
"devDependencies": {
"@rollup/plugin-commonjs": "^21.1.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.2.1",
"@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-replace": "^4.0.0",
"@rollup/plugin-typescript": "^8.3.2",
"@rollup/plugin-typescript": "^8.3.3",
"@types/react": "16.14.0",
"@types/react-router": "5.1.18",
"@types/webpack": "^5.28.0",
"husky": "^8.0.1",
"import-sort-style-module": "^6.0.0",
"prettier": "^2.6.2",
"inquirer": "^8.2.4",
"prettier": "^2.7.1",
"prettier-plugin-import-sort": "^0.0.7",
"react": "16.14.0",
"react-dom": "16.14.0",
"rollup": "^2.70.2",
"rollup": "^2.76.0",
"tslib": "^2.4.0",
"typescript": "^4.7.2"
"typescript": "^4.7.4"
},
"importSort": {
".js, .jsx, .ts, .tsx": {
@@ -36,7 +37,7 @@
}
},
"dependencies": {
"decky-frontend-lib": "^0.0.6",
"react-icons": "^4.3.1"
"decky-frontend-lib": "^1.2.1",
"react-icons": "^4.4.0"
}
}
+1738
View File
File diff suppressed because it is too large Load Diff
+15 -33
View File
@@ -1,47 +1,29 @@
import { ButtonItem, DialogButton, PanelSection, PanelSectionRow, Router } from 'decky-frontend-lib';
import { ButtonItem, PanelSection, PanelSectionRow } from 'decky-frontend-lib';
import { VFC } from 'react';
import { FaArrowLeft, FaStore } from 'react-icons/fa';
import { useDeckyState } from './DeckyState';
const PluginView: VFC = () => {
const { plugins, activePlugin, setActivePlugin, closeActivePlugin } = useDeckyState();
const onStoreClick = () => {
Router.CloseSideMenus();
Router.NavigateToExternalWeb('http://127.0.0.1:1337/browser/redirect');
};
const { plugins, activePlugin, setActivePlugin } = useDeckyState();
if (activePlugin) {
return (
<div style={{ height: '100%' }}>
<div style={{ position: 'absolute', top: '3px', left: '16px', zIndex: 20 }}>
<DialogButton style={{ minWidth: 0, padding: '10px 12px' }} onClick={closeActivePlugin}>
<FaArrowLeft style={{ display: 'block' }} />
</DialogButton>
</div>
{activePlugin.content}
</div>
);
return <div style={{ height: '100%' }}>{activePlugin.content}</div>;
}
return (
<PanelSection>
<div style={{ position: 'absolute', top: '3px', right: '16px', zIndex: 20 }}>
<DialogButton style={{ minWidth: 0, padding: '10px 12px' }} onClick={onStoreClick}>
<FaStore style={{ display: 'block' }} />
</DialogButton>
</div>
{plugins.map(({ name, icon }) => (
<PanelSectionRow key={name}>
<ButtonItem layout="below" onClick={() => setActivePlugin(name)}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>{icon}</div>
<div>{name}</div>
</div>
</ButtonItem>
</PanelSectionRow>
))}
{plugins
.filter((p) => p.content)
.map(({ name, icon }) => (
<PanelSectionRow key={name}>
<ButtonItem layout="below" onClick={() => setActivePlugin(name)}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>{icon}</div>
<div>{name}</div>
</div>
</ButtonItem>
</PanelSectionRow>
))}
</PanelSection>
);
};
+47 -6
View File
@@ -1,18 +1,59 @@
import { staticClasses } from 'decky-frontend-lib';
import { VFC } from 'react';
import { DialogButton, Focusable, Router, staticClasses } from 'decky-frontend-lib';
import { CSSProperties, VFC } from 'react';
import { FaArrowLeft, FaCog, FaStore } from 'react-icons/fa';
import { useDeckyState } from './DeckyState';
const titleStyles: CSSProperties = {
display: 'flex',
paddingTop: '3px',
paddingBottom: '14px',
paddingRight: '16px',
boxShadow: 'unset',
};
const TitleView: VFC = () => {
const { activePlugin } = useDeckyState();
const { activePlugin, closeActivePlugin } = useDeckyState();
const onSettingsClick = () => {
Router.CloseSideMenus();
Router.Navigate('/decky/settings');
};
const onStoreClick = () => {
Router.CloseSideMenus();
Router.Navigate('/decky/store');
};
if (activePlugin === null) {
return <div className={staticClasses.Title}>Decky</div>;
return (
<Focusable style={titleStyles} className={staticClasses.Title}>
<DialogButton
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
onClick={onSettingsClick}
>
<FaCog style={{ marginTop: '-4px', display: 'block' }} />
</DialogButton>
<div style={{ marginRight: 'auto', flex: 0.9 }}>Decky</div>
<DialogButton
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
onClick={onStoreClick}
>
<FaStore style={{ marginTop: '-4px', display: 'block' }} />
</DialogButton>
</Focusable>
);
}
return (
<div className={staticClasses.Title} style={{ paddingLeft: '60px' }}>
{activePlugin.name}
<div className={staticClasses.Title} style={titleStyles}>
<DialogButton
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
onClick={closeActivePlugin}
>
<FaArrowLeft style={{ marginTop: '-4px', display: 'block' }} />
</DialogButton>
<div style={{ flex: 0.9 }}>{activePlugin.name}</div>
</div>
);
};
@@ -0,0 +1,25 @@
import { SidebarNavigation } from 'decky-frontend-lib';
import GeneralSettings from './pages/general';
import PluginList from './pages/plugin_list';
export default function SettingsPage() {
return (
<SidebarNavigation
title="Decky Settings"
showTitle
pages={[
{
title: 'General',
content: <GeneralSettings />,
route: '/decky/settings/general',
},
{
title: 'Plugins',
content: <PluginList />,
route: '/decky/settings/plugins',
},
]}
/>
);
}
@@ -0,0 +1,97 @@
import { DialogButton, Field, ProgressBarWithInfo, Spinner } from 'decky-frontend-lib';
import { useEffect, useState } from 'react';
import { FaArrowDown } from 'react-icons/fa';
import { callUpdaterMethod, finishUpdate } from '../../../../updater';
interface VerInfo {
current: string;
remote: {
assets: {
browser_download_url: string;
created_at: string;
}[];
name: string;
body: string;
prerelease: boolean;
published_at: string;
tag_name: string;
} | null;
updatable: boolean;
}
export default function UpdaterSettings() {
const [versionInfo, setVersionInfo] = useState<VerInfo | null>(null);
const [updateProgress, setUpdateProgress] = useState<number>(-1);
const [reloading, setReloading] = useState<boolean>(false);
const [checkingForUpdates, setCheckingForUpdates] = useState<boolean>(false);
useEffect(() => {
(async () => {
const res = (await callUpdaterMethod('get_version')) as { result: VerInfo };
setVersionInfo(res.result);
})();
}, []);
return (
<Field
label="Updates"
description={
versionInfo && (
<span style={{ whiteSpace: 'pre-line' }}>{`Current version: ${versionInfo.current}\n${
versionInfo.updatable ? `Latest version: ${versionInfo.remote?.tag_name}` : ''
}`}</span>
)
}
icon={
!versionInfo ? (
<Spinner style={{ width: '1em', height: 20, display: 'block' }} />
) : (
<FaArrowDown style={{ display: 'block' }} />
)
}
>
{updateProgress == -1 ? (
<DialogButton
disabled={!versionInfo?.updatable || checkingForUpdates}
onClick={
!versionInfo?.remote || versionInfo?.remote?.tag_name == versionInfo?.current
? async () => {
setCheckingForUpdates(true);
const res = (await callUpdaterMethod('check_for_updates')) as { result: VerInfo };
setVersionInfo(res.result);
setCheckingForUpdates(false);
}
: async () => {
window.DeckyUpdater = {
updateProgress: (i) => {
setUpdateProgress(i);
},
finish: async () => {
setUpdateProgress(0);
setReloading(true);
await finishUpdate();
},
};
setUpdateProgress(0);
callUpdaterMethod('do_update');
}
}
>
{checkingForUpdates
? 'Checking'
: !versionInfo?.remote || versionInfo?.remote?.tag_name == versionInfo?.current
? 'Check For Updates'
: 'Install Update'}
</DialogButton>
) : (
<ProgressBarWithInfo
layout="inline"
bottomSeparator={false}
nProgress={updateProgress}
indeterminate={reloading}
sOperationText={reloading ? 'Reloading' : 'Updating'}
/>
)}
</Field>
);
}
@@ -0,0 +1,34 @@
import { DialogButton, Field, TextField } from 'decky-frontend-lib';
import { useState } from 'react';
import { FaShapes } from 'react-icons/fa';
import { installFromURL } from '../../../store/Store';
import UpdaterSettings from './Updater';
export default function GeneralSettings() {
const [pluginURL, setPluginURL] = useState('');
// const [checked, setChecked] = useState(false); // store these in some kind of State instead
return (
<div>
{/* <Field
label="A Toggle with an icon"
icon={<FaShapes style={{ display: 'block' }} />}
>
<Toggle
value={checked}
onChange={(e) => setChecked(e)}
/>
</Field> */}
<UpdaterSettings />
<Field
label="Manual plugin install"
description={<TextField label={'URL'} value={pluginURL} onChange={(e) => setPluginURL(e?.target.value)} />}
icon={<FaShapes style={{ display: 'block' }} />}
>
<DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}>
Install
</DialogButton>
</Field>
</div>
);
}
@@ -0,0 +1,34 @@
import { DialogButton, staticClasses } from 'decky-frontend-lib';
import { FaTrash } from 'react-icons/fa';
import { useDeckyState } from '../../../DeckyState';
export default function PluginList() {
const { plugins } = useDeckyState();
if (plugins.length === 0) {
return (
<div>
<p>No plugins installed</p>
</div>
);
}
return (
<ul style={{ listStyleType: 'none' }}>
{plugins.map(({ name }) => (
<li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<span>{name}</span>
<div className={staticClasses.Title} style={{ marginLeft: 'auto', boxShadow: 'none' }}>
<DialogButton
style={{ height: '40px', width: '40px', padding: '10px 12px' }}
onClick={() => window.DeckyPluginLoader.uninstall_plugin(name)}
>
<FaTrash />
</DialogButton>
</div>
</li>
))}
</ul>
);
}
@@ -0,0 +1,210 @@
import {
DialogButton,
Dropdown,
Focusable,
QuickAccessTab,
Router,
SingleDropdownOption,
SuspensefulImage,
staticClasses,
} from 'decky-frontend-lib';
import { FC, useRef, useState } from 'react';
import {
LegacyStorePlugin,
StorePlugin,
StorePluginVersion,
requestLegacyPluginInstall,
requestPluginInstall,
} from './Store';
interface PluginCardProps {
plugin: StorePlugin | LegacyStorePlugin;
}
const classNames = (...classes: string[]) => {
return classes.join(' ');
};
function isLegacyPlugin(plugin: LegacyStorePlugin | StorePlugin): plugin is LegacyStorePlugin {
return 'artifact' in plugin;
}
const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
const [selectedOption, setSelectedOption] = useState<number>(0);
const buttonRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
return (
<div
style={{
padding: '30px',
paddingTop: '10px',
paddingBottom: '10px',
}}
>
{/* TODO: abstract this messy focus hackiness into a custom component in lib */}
<Focusable
// className="Panel Focusable"
ref={containerRef}
onActivate={(_: CustomEvent) => {
buttonRef.current!.focus();
}}
onCancel={(_: CustomEvent) => {
if (containerRef.current!.querySelectorAll('* :focus').length === 0) {
Router.NavigateBackOrOpenMenu();
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 1000);
} else {
containerRef.current!.focus();
}
}}
style={{
display: 'flex',
flexDirection: 'column',
background: '#ACB2C924',
height: 'unset',
marginBottom: 'unset',
// boxShadow: var(--gpShadow-Medium);
scrollSnapAlign: 'start',
boxSizing: 'border-box',
}}
>
<div style={{ display: 'flex', alignItems: 'center' }}>
<a
style={{ fontSize: '18pt', padding: '10px' }}
className={classNames(staticClasses.Text)}
// onClick={() => Router.NavigateToExternalWeb('https://github.com/' + plugin.artifact)}
>
{isLegacyPlugin(plugin) ? (
<div>
<span style={{ color: 'grey' }}>{plugin.artifact.split('/')[0]}/</span>
{plugin.artifact.split('/')[1]}
</div>
) : (
plugin.name
)}
</a>
</div>
<div
style={{
display: 'flex',
flexDirection: 'row',
}}
>
<SuspensefulImage
suspenseWidth="256px"
style={{
width: 'auto',
height: '160px',
}}
src={
isLegacyPlugin(plugin)
? `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/artifact_images/${plugin.artifact.replace(
'/',
'_',
)}.png`
: `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/artifact_images/${plugin.name.replace(
'/',
'_',
)}.png`
}
/>
<div
style={{
display: 'flex',
flexDirection: 'column',
}}
>
<p className={classNames(staticClasses.PanelSectionRow)}>
<span>Author: {plugin.author}</span>
</p>
<p className={classNames(staticClasses.PanelSectionRow)}>
<span>Tags:</span>
{plugin.tags.map((tag: string) => (
<span
style={{
padding: '5px',
marginRight: '10px',
borderRadius: '5px',
background: tag == 'root' ? '#842029' : '#ACB2C947',
}}
>
{tag == 'root' ? 'Requires root' : tag}
</span>
))}
{isLegacyPlugin(plugin) && (
<span
style={{
color: '#232120',
padding: '5px',
marginRight: '10px',
borderRadius: '5px',
background: '#EDE841',
}}
>
legacy
</span>
)}
</p>
</div>
</div>
<div
style={{
width: '100%',
alignSelf: 'flex-end',
display: 'flex',
flexDirection: 'row',
}}
>
<Focusable
style={{
display: 'flex',
flexDirection: 'row',
width: '100%',
}}
>
<div
style={{
flex: '1',
}}
>
<DialogButton
ref={buttonRef}
onClick={() =>
isLegacyPlugin(plugin)
? requestLegacyPluginInstall(plugin, Object.keys(plugin.versions)[selectedOption])
: requestPluginInstall(plugin, plugin.versions[selectedOption])
}
>
Install
</DialogButton>
</div>
<div
style={{
flex: '0.2',
}}
>
<Dropdown
rgOptions={
(isLegacyPlugin(plugin)
? Object.keys(plugin.versions).map((v, k) => ({
data: k,
label: v,
}))
: plugin.versions.map((version: StorePluginVersion, index) => ({
data: index,
label: version.name,
}))) as SingleDropdownOption[]
}
strDefaultLabel={'Select a version'}
selectedOption={selectedOption}
onChange={({ data }) => setSelectedOption(data)}
/>
</div>
</Focusable>
</div>
</Focusable>
</div>
);
};
export default PluginCard;
+135
View File
@@ -0,0 +1,135 @@
import { ModalRoot, SteamSpinner, showModal, staticClasses } from 'decky-frontend-lib';
import { FC, useEffect, useState } from 'react';
import PluginCard from './PluginCard';
export interface StorePluginVersion {
name: string;
hash: string;
}
export interface StorePlugin {
id: number;
name: string;
versions: StorePluginVersion[];
author: string;
description: string;
tags: string[];
}
export interface LegacyStorePlugin {
artifact: string;
versions: {
[version: string]: string;
};
author: string;
description: string;
tags: string[];
}
export async function installFromURL(url: string) {
const formData = new FormData();
const splitURL = url.split('/');
formData.append('name', splitURL[splitURL.length - 1].replace('.zip', ''));
formData.append('artifact', url);
await fetch('http://localhost:1337/browser/install_plugin', {
method: 'POST',
body: formData,
});
}
export function requestLegacyPluginInstall(plugin: LegacyStorePlugin, selectedVer: string) {
showModal(
<ModalRoot
onOK={() => {
const formData = new FormData();
formData.append('name', plugin.artifact);
formData.append('artifact', `https://github.com/${plugin.artifact}/archive/refs/tags/${selectedVer}.zip`);
formData.append('version', selectedVer);
formData.append('hash', plugin.versions[selectedVer]);
fetch('http://localhost:1337/browser/install_plugin', {
method: 'POST',
body: formData,
});
}}
onCancel={() => {
// do nothing
}}
>
<div className={staticClasses.Title} style={{ flexDirection: 'column' }}>
Using legacy plugins
</div>
You are currently installing a <b>legacy</b> plugin. Legacy plugins are no longer supported and may have issues.
Legacy plugins do not support gamepad input. To interact with a legacy plugin, you will need to use the
touchscreen.
</ModalRoot>,
);
}
export async function requestPluginInstall(plugin: StorePlugin, selectedVer: StorePluginVersion) {
const formData = new FormData();
formData.append('name', plugin.name);
formData.append('artifact', `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${selectedVer.hash}.zip`);
formData.append('version', selectedVer.name);
formData.append('hash', selectedVer.hash);
await fetch('http://localhost:1337/browser/install_plugin', {
method: 'POST',
body: formData,
});
}
const StorePage: FC<{}> = () => {
const [data, setData] = useState<StorePlugin[] | null>(null);
const [legacyData, setLegacyData] = useState<LegacyStorePlugin[] | null>(null);
useEffect(() => {
(async () => {
const res = await fetch('https://beta.deckbrew.xyz/plugins', { method: 'GET' }).then((r) => r.json());
console.log(res);
setData(res.filter((x: StorePlugin) => x.name !== 'Example Plugin'));
})();
(async () => {
const res = await fetch('https://plugins.deckbrew.xyz/get_plugins', { method: 'GET' }).then((r) => r.json());
console.log(res);
setLegacyData(res);
})();
}, []);
return (
<div
style={{
marginTop: '40px',
height: 'calc( 100% - 40px )',
overflowY: 'scroll',
}}
>
<div
style={{
display: 'flex',
flexWrap: 'nowrap',
flexDirection: 'column',
height: '100%',
}}
>
{!data ? (
<div style={{ height: '100%' }}>
<SteamSpinner />
</div>
) : (
<div>
{data.map((plugin: StorePlugin) => (
<PluginCard plugin={plugin} />
))}
{!legacyData ? (
<SteamSpinner />
) : (
legacyData.map((plugin: LegacyStorePlugin) => <PluginCard plugin={plugin} />)
)}
</div>
)}
</div>
</div>
);
};
export default StorePage;
+3
View File
@@ -1,14 +1,17 @@
import PluginLoader from './plugin-loader';
import { DeckyUpdater } from './updater';
declare global {
interface Window {
DeckyPluginLoader: PluginLoader;
DeckyUpdater?: DeckyUpdater;
importDeckyPlugin: Function;
syncDeckyPlugins: Function;
}
}
window.DeckyPluginLoader?.dismountAll();
window.DeckyPluginLoader?.deinit();
window.DeckyPluginLoader = new PluginLoader();
window.importDeckyPlugin = function (name: string) {
+74 -28
View File
@@ -1,9 +1,11 @@
import { ModalRoot, showModal, staticClasses } from 'decky-frontend-lib';
import { ModalRoot, QuickAccessTab, Router, showModal, sleep, staticClasses } from 'decky-frontend-lib';
import { FaPlug } from 'react-icons/fa';
import { DeckyState, DeckyStateContextProvider } from './components/DeckyState';
import LegacyPlugin from './components/LegacyPlugin';
import PluginView from './components/PluginView';
import SettingsPage from './components/settings';
import StorePage from './components/store/Store';
import TitleView from './components/TitleView';
import Logger from './logger';
import { Plugin } from './plugin';
@@ -30,35 +32,66 @@ class PluginLoader extends Logger {
this.log('Initialized');
this.tabsHook.add({
id: 'main',
title: (
<DeckyStateContextProvider deckyState={this.deckyState}>
<TitleView />
</DeckyStateContextProvider>
),
id: QuickAccessTab.Decky,
title: null,
content: (
<DeckyStateContextProvider deckyState={this.deckyState}>
<TitleView />
<PluginView />
</DeckyStateContextProvider>
),
icon: <FaPlug />,
});
this.routerHook.addRoute('/decky/store', () => <StorePage />);
this.routerHook.addRoute('/decky/settings', () => {
return (
<DeckyStateContextProvider deckyState={this.deckyState}>
<SettingsPage />
</DeckyStateContextProvider>
);
});
}
public addPluginInstallPrompt(artifact: string, version: string, request_id: string) {
public addPluginInstallPrompt(artifact: string, version: string, request_id: string, hash: string) {
showModal(
<ModalRoot
onOK={() => {
console.log('ok');
this.callServerMethod('confirm_plugin_install', { request_id });
onOK={async () => {
await this.callServerMethod('confirm_plugin_install', { request_id });
Router.NavigateBackOrOpenMenu();
await sleep(250);
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 1000);
}}
onCancel={() => {
console.log('nope');
this.callServerMethod('cancel_plugin_install', { request_id });
}}
>
<div className={staticClasses.Title}>
Install {artifact} version {version}?
<div className={staticClasses.Title} style={{ flexDirection: 'column' }}>
{hash == 'False' ? <h3 style={{ color: 'red' }}>!!!!NO HASH PROVIDED!!!!</h3> : null}
Install {artifact}
{version ? ' version ' + version : null}?
</div>
</ModalRoot>,
);
}
public uninstall_plugin(name: string) {
showModal(
<ModalRoot
onOK={async () => {
const formData = new FormData();
formData.append('name', name);
await fetch('http://localhost:1337/browser/uninstall_plugin', {
method: 'POST',
body: formData,
});
}}
onCancel={() => {
// do nothing
}}
>
<div className={staticClasses.Title} style={{ flexDirection: 'column' }}>
Uninstall {name}?
</div>
</ModalRoot>,
);
@@ -71,25 +104,39 @@ class PluginLoader extends Logger {
}
}
public async importPlugin(name: string) {
try {
if (this.reloadLock) {
this.log('Reload currently in progress, adding to queue', name);
this.pluginReloadQueue.push(name);
return;
}
public deinit() {
this.routerHook.removeRoute('/decky/store');
this.routerHook.removeRoute('/decky/settings');
}
public unloadPlugin(name: string) {
const plugin = this.plugins.find((plugin) => plugin.name === name || plugin.name === name.replace('$LEGACY_', ''));
plugin?.onDismount?.();
this.plugins = this.plugins.filter((p) => p !== plugin);
this.deckyState.setPlugins(this.plugins);
}
public async importPlugin(name: string) {
if (this.reloadLock) {
this.log('Reload currently in progress, adding to queue', name);
this.pluginReloadQueue.push(name);
return;
}
try {
this.reloadLock = true;
this.log(`Trying to load ${name}`);
let find = this.plugins.find((x) => x.name == name);
if (find) this.plugins.splice(this.plugins.indexOf(find), 1);
this.unloadPlugin(name);
if (name.startsWith('$LEGACY_')) {
await this.importLegacyPlugin(name.replace('$LEGACY_', ''));
} else {
await this.importReactPlugin(name);
}
this.log(`Loaded ${name}`);
this.deckyState.setPlugins(this.plugins);
this.log(`Loaded ${name}`);
} catch (e) {
throw e;
} finally {
@@ -104,11 +151,10 @@ class PluginLoader extends Logger {
private async importReactPlugin(name: string) {
let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`);
if (res.ok) {
let content = await eval(await res.text())(this.createPluginAPI(name));
let plugin = await eval(await res.text())(this.createPluginAPI(name));
this.plugins.push({
...plugin,
name: name,
icon: content.icon,
content: content.content,
});
} else throw new Error(`${name} frontend_bundle not OK`);
}
@@ -152,7 +198,7 @@ class PluginLoader extends Logger {
return response.json();
},
fetchNoCors(url: string, request: any = {}) {
let args = { method: 'POST', headers: {}, body: '' };
let args = { method: 'POST', headers: {} };
const req = { ...args, ...request, url, data: request.body };
return this.callServerMethod('http_request', req);
},
+3 -3
View File
@@ -1,6 +1,6 @@
export interface Plugin {
name: any;
content: any;
icon: any;
name: string;
icon: JSX.Element;
content?: JSX.Element;
onDismount?(): void;
}
+4
View File
@@ -92,6 +92,10 @@ class RouterHook extends Logger {
this.routerState.addRoute(path, component, props);
}
removeRoute(path: string) {
this.routerState.removeRoute(path);
}
deinit() {
unpatch(this.gamepadWrapper, 'render');
this.router && unpatch(this.router, 'type');
+77 -12
View File
@@ -1,3 +1,6 @@
import { QuickAccessTab, afterPatch, sleep, unpatch } from 'decky-frontend-lib';
import { memo } from 'react';
import Logger from './logger';
declare global {
@@ -11,11 +14,11 @@ declare global {
const isTabsArray = (tabs: any) => {
const length = tabs.length;
return length === 7 && tabs[length - 1]?.key === 6 && tabs[length - 1]?.tab;
return length >= 7 && tabs[length - 1]?.tab;
};
interface Tab {
id: string;
id: QuickAccessTab | number;
title: any;
content: any;
icon: any;
@@ -24,24 +27,86 @@ interface Tab {
class TabsHook extends Logger {
// private keys = 7;
tabs: Tab[] = [];
private quickAccess: any;
private tabRenderer: any;
private memoizedQuickAccess: any;
private cNode: any;
private qAPTree: any;
private rendererTree: any;
constructor() {
super('TabsHook');
this.log('Initialized');
window.__TABS_HOOK_INSTANCE?.deinit?.();
window.__TABS_HOOK_INSTANCE = this;
const self = this;
const filter = Array.prototype.__filter ?? Array.prototype.filter;
Array.prototype.__filter = filter;
Array.prototype.filter = function (...args: any[]) {
if (isTabsArray(this)) {
self.render(this);
const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current;
let scrollRoot: any;
let currentNode = tree;
(async () => {
let iters = 0;
while (!scrollRoot) {
iters++;
currentNode = currentNode?.child;
if (iters >= 30 || !currentNode) {
iters = 0;
currentNode = tree;
await sleep(5000);
}
if (currentNode?.type?.prototype?.RemoveSmartScrollContainer) scrollRoot = currentNode;
}
// @ts-ignore
return filter.call(this, ...args);
};
let newQA: any;
let newQATabRenderer: any;
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 = (...args: any) => {
const oFilter = Array.prototype.filter;
Array.prototype.filter = function (...args: any[]) {
if (isTabsArray(this)) {
self.render(this);
}
// @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(...args);
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();
})();
}
deinit() {
unpatch(this.cNode.stateNode, 'render');
if (this.qAPTree) this.qAPTree.type = this.quickAccess;
if (this.rendererTree) this.rendererTree.type = this.tabRenderer;
if (this.cNode) this.cNode.stateNode.forceUpdate();
}
add(tab: Tab) {
@@ -49,7 +114,7 @@ class TabsHook extends Logger {
this.tabs.push(tab);
}
removeById(id: string) {
removeById(id: number) {
this.log('Removing tab', id);
this.tabs = this.tabs.filter((tab) => tab.id !== id);
}
+30
View File
@@ -0,0 +1,30 @@
import { sleep } from 'decky-frontend-lib';
export enum Branches {
Release,
Prerelease,
Nightly,
}
export interface DeckyUpdater {
updateProgress: (val: number) => void;
finish: () => void;
}
export async function callUpdaterMethod(methodName: string, args = {}) {
const response = await fetch(`http://127.0.0.1:1337/updater/${methodName}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(args),
});
return response.json();
}
export async function finishUpdate() {
callUpdaterMethod('do_restart');
await sleep(3000);
location.reload();
}
+1
View File
@@ -5,6 +5,7 @@
"target": "ES2020",
"jsx": "react",
"jsxFactory": "window.SP_REACT.createElement",
"jsxFragmentFactory": "window.SP_REACT.Fragment",
"declaration": false,
"moduleResolution": "node",
"noUnusedLocals": true,
+1
View File
@@ -2,3 +2,4 @@ aiohttp==3.8.1
aiohttp-jinja2==1.5.0
aiohttp_cors==0.7.0
watchdog==2.1.7
certifi==2022.6.15