mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-17 08:47:49 +00:00
Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e88c7c9ac | |||
| f015e00561 | |||
| e07827cdb5 | |||
| 103d43e7c9 | |||
| 23b7df0ce2 | |||
| a5671e19ce | |||
| f2fbd399fe | |||
| 28b91963a9 | |||
| ce2268370f | |||
| 59462041b1 | |||
| d4d32c8d55 | |||
| e600aeccc7 | |||
| 162d1b561b | |||
| ba824fc921 | |||
| 8c8cf180fa | |||
| 05d11cfff0 | |||
| 3c24b37247 | |||
| dbb4bc5ab4 | |||
| b00b04ceeb | |||
| 470f16adda | |||
| 76424174ed | |||
| b618fe1e97 | |||
| 45949e8456 | |||
| e3a965329d | |||
| 6ee41578ea | |||
| 9404215399 | |||
| b8bf150a74 | |||
| add3f77c1a | |||
| 6c42661f86 | |||
| 2b3c219e38 | |||
| 8eb89da373 | |||
| ace9f61e50 | |||
| baa02c129f | |||
| 1e6b3edbf2 | |||
| 085aacea06 | |||
| 675e667a9e | |||
| 58b2c4208d | |||
| c2693869a7 | |||
| 683c51ceac | |||
| 630e8b7213 | |||
| 246b31794a | |||
| b7d57de378 | |||
| ee8aa98446 | |||
| 557a00aed7 | |||
| 4daf028e7a | |||
| 934a50f683 | |||
| aa4f1b1e87 | |||
| 67495d30d6 | |||
| d72f364a8d | |||
| da0f7dd337 | |||
| 518b01f571 | |||
| 3f2a2bbc04 | |||
| 79e8af8be6 | |||
| 18d444e8fc | |||
| abc5ce5382 | |||
| 9619c52720 | |||
| 80b223180e | |||
| 1d5d14b492 | |||
| ce23534ccc | |||
| e6e74d8e9d | |||
| 6289578f68 | |||
| e7c44ee202 | |||
| 39f6a7688d | |||
| 47ca3ece4a | |||
| 3e250dd180 | |||
| 711af3bca3 | |||
| 9a6930571c | |||
| d9dd09c69b | |||
| daca482ed8 | |||
| 99b4b939bd |
+128
-19
@@ -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
@@ -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/*
|
||||
+12
@@ -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
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"deckip" : "0.0.0.0",
|
||||
"deckport" : "22",
|
||||
"deckpass" : "ssap",
|
||||
"deckkey" : "-i ${env:HOME}/.ssh/id_rsa",
|
||||
"deckdir" : "/home/deck"
|
||||
}
|
||||
Vendored
+12
-3
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+139
-5
@@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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 [](https://discord.gg/ZU74G2NJzk)
|
||||
|
||||

|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"]
|
||||
|
||||
@@ -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"])
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
|
||||
+45
@@ -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
|
||||
Generated
-3881
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1738
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export interface Plugin {
|
||||
name: any;
|
||||
content: any;
|
||||
icon: any;
|
||||
name: string;
|
||||
icon: JSX.Element;
|
||||
content?: JSX.Element;
|
||||
onDismount?(): void;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
"target": "ES2020",
|
||||
"jsx": "react",
|
||||
"jsxFactory": "window.SP_REACT.createElement",
|
||||
"jsxFragmentFactory": "window.SP_REACT.Fragment",
|
||||
"declaration": false,
|
||||
"moduleResolution": "node",
|
||||
"noUnusedLocals": true,
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user