Compare commits

...

66 Commits

Author SHA1 Message Date
Sefa Eyeoglu 43dee863cd Add CEF Remote Debugging toggle (#129)
* feat: add CEF Remote Debugging toggle

* feat: disable remote debugger on startup

* refactor: stop debugger instead of disable

* feat: add option to allow remote debugging by default

Co-authored-by: TrainDoctor <traindoctor@protonmail.com>
2022-08-18 14:50:59 -07:00
TrainDoctor 55a7682663 fix ButtonItem shim applying when not needed 2022-08-17 17:32:50 -07:00
TrainDoctor d05e8d36b4 Update to latest decky-frontend-lib 2022-08-17 17:28:30 -07:00
AAGaming 0018b8e957 bump lib and add temporary shims for webpack v5 2022-08-17 20:03:45 -04:00
TrainDoctor 59038f65ac Fix log spam from injection related errors 2022-08-17 12:57:58 -07:00
AAGaming 5960c11d60 add class names to PluginCard for theming 2022-08-17 15:27:22 -04:00
Sefa Eyeoglu 8d065eab1f Add Plugin Reload Button to Settings (#128)
* feat: add reload button to plugin list

Signed-off-by: Sefa Eyeoglu <contact@scrumplex.net>

* refactor: move plugin actions into context menu

Signed-off-by: Sefa Eyeoglu <contact@scrumplex.net>
2022-08-16 16:51:39 -07:00
AAGaming 3b1b6d28d6 add some classes for nicer scrolling, update lib 2022-08-15 13:22:38 -04:00
AAGaming 0a735886c9 fix toasts breaking sometimes 2022-08-14 21:59:55 -04:00
AAGaming c9430f5be4 less stupid method 2022-08-14 13:17:39 -04:00
AAGaming a4e2237fc0 fix loader not re-injecting on restart 2022-08-14 12:51:07 -04:00
AAGaming 85d0398e62 shut typescript up 2022-08-14 00:02:01 -04:00
AAGaming 30a538e85e FINALLY fix the multiple injections bug 2022-08-13 23:58:57 -04:00
AAGaming 84a19203c5 fix injecting twice 2022-08-13 11:57:52 -04:00
AAGaming 99cda2907d fix TS errors 2022-08-12 21:02:11 -04:00
AAGaming a38582d158 Fix toaster deinit error 2022-08-12 16:49:28 -04:00
TrainDoctor 9556994e14 fix empty settings and store screens after reboot 2022-08-12 11:45:29 -07:00
OMGDuke dee2cfa47b remove console.log that was causing lots of log spam (#138) 2022-08-12 09:54:57 -04:00
TrainDoctor 463403be23 Update build.yml 2022-08-11 20:37:46 -07:00
TrainDoctor b68eaca55d Updater should now find all version tags 2022-08-11 20:12:17 -07:00
AAGaming 114c54c9b0 Fix route unpatching 2022-08-11 20:34:55 -04:00
TrainDoctor 47e0661773 Add releases back to the mix 2022-08-11 17:10:37 -07:00
TrainDoctor 6c48dfe7f6 Actually send the proper variable out 2022-08-11 16:54:13 -07:00
TrainDoctor ed0ae7c9e2 Removed un-needed trimming 2022-08-11 16:48:50 -07:00
TrainDoctor ea265ae6df Corrected dummy tag, added echoing 2022-08-11 16:18:21 -07:00
TrainDoctor 860caf440b Add semver tool, temporarily disable triggered pre-releases 2022-08-11 16:10:00 -07:00
TrainDoctor 64040879f5 Update to latest version of decky-frontend-lib 2022-08-10 15:48:48 -07:00
AAGaming e92073162a oops: remove test log 2022-08-10 16:34:53 -04:00
AAGaming 67426af3ef Add api for showing toast notifications 2022-08-09 21:52:03 -04:00
Sefa Eyeoglu 0dbdb4a143 fix: don't pass unzip job to event loop (#136)
For some reason this broke installation of plugins when another specific
plugin was present (vibrantDeck)
2022-08-09 12:06:33 -07:00
TrainDoctor c9e9c45b37 Standardize logging in browser.py 2022-08-08 13:06:04 -07:00
TrainDoctor 6bc8a4fb1d Add missing import 2022-08-08 12:38:35 -07:00
Derek J. Clark 20094c5f75 Use Environment Variables (#123)
Uses environment variables instead of hard coding the "deck" user/group.
This adds support for systems other than the steam deck that are using the DeckUI.

* Use Environment Variables

* Use method to get USER from a systemd root process

* Fix imports. Add get_user and get_user_group methods in helpers.py. Removed duplicated code

* Add separate setters/getters for user vars. Ensure sleep prevents race condition of user setter in while loop
2022-08-08 11:32:14 -07:00
AAGaming 198591dbd7 whoops don't need it here 2022-08-05 21:18:19 -04:00
AAGaming f21d34506d Implement CSRF protection 2022-08-05 21:16:29 -04:00
AAGaming ab6ec98160 API for patching existing routes, lower power use 2022-08-02 18:54:55 -04:00
Beebles f1e809781a forgot one update (#125) 2022-07-30 19:42:16 -07:00
Beebles 789058b72f Readme.md references incorrect github repo (#124) 2022-07-30 18:40:44 -07:00
TrainDoctor 4a68b1430d Update README.md 2022-07-28 13:50:03 -07:00
TrainDoctor 66c4a7e16e Update README.md 2022-07-25 17:29:56 -07:00
TrainDoctor b929b2dddf Update README.md 2022-07-25 17:08:10 -07:00
TrainDoctor fb0b703438 Fix unintended question mark in "Installing" modal 2022-07-25 16:07:16 -07:00
AAGaming afb2c7c0ed Better install process UX, fix reinstalling 2022-07-25 17:13:50 -04:00
AAGaming 52dded85ed quick fix for routes refreshing constantly 2022-07-24 11:51:42 -04:00
AAGaming 2004bdebbf fix calibration menu in controller settings 2022-07-24 11:37:38 -04:00
AAGaming c9bf8d357e use fstring 2022-07-21 22:03:11 -04:00
AAGaming 09eee761a5 change log to debug 2022-07-21 22:02:47 -04:00
AAGaming 20f43b2fd4 fix plugin uninstalling 2022-07-21 22:02:13 -04:00
AAGaming e6dd1c29d8 remove modal box shadow 2022-07-17 16:42:24 -04:00
AAGaming 6e88c7c9ac Show warning when installing legacy plugins 2022-07-17 16:09:42 -04:00
AAGaming f015e00561 more updater fixes 2022-07-15 12:57:51 -04:00
AAGaming e07827cdb5 catch rm errors 2022-07-15 12:36:16 -04:00
AAGaming 103d43e7c9 fix updater 2022-07-15 12:31:30 -04:00
AAGaming 23b7df0ce2 wait 30s before first update check 2022-07-15 12:25:27 -04:00
AAGaming a5671e19ce fix ci AGAIN 2022-07-15 12:20:05 -04:00
AAGaming f2fbd399fe allow users to manually check for updates 2022-07-15 12:16:57 -04:00
AAGaming 28b91963a9 fix ci part 4
fix ci part 5

fix ci part 6

fix ci part 7

fix ci part 8

fix ci part 9

fix ci part 10

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

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

* add chmod

* remove junk comments
2022-07-14 22:51:55 -04:00
AAGaming 05d11cfff0 fix get_tabs oopsie 2022-07-13 23:24:29 -04:00
botato 3c24b37247 change ci again (#116) 2022-07-13 21:19:19 -04:00
36 changed files with 1293 additions and 349 deletions
+109 -21
View File
@@ -3,8 +3,8 @@ name: Builder
on:
push:
pull_request:
schedule:
- cron: '0 13 * * *' # run at 1 PM UTC
# schedule:
# - cron: '0 13 * * *' # run at 1 PM UTC
workflow_dispatch:
inputs:
release:
@@ -75,12 +75,6 @@ jobs:
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:
@@ -89,9 +83,9 @@ jobs:
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') }}
prerelease:
name: Release the pre-release version of the package
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release == 'prerelease' }}
needs: build
runs-on: ubuntu-latest
@@ -104,21 +98,115 @@ jobs:
with:
name: PluginLoader
path: dist
- name: Bump version and push tag ⏫
id: tag_version
uses: mathieudutour/github-tag-action@v6.0
- name: Install semver-tool asdf
uses: asdf-vm/actions/install@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
release_branches: ''
pre_release_branches: 'main'
append_to_pre_release_tag: '-pre'
tool_versions: |
semver 3.3.0
- 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 }}
echo "VERS: $VERSION"
OUT=$(semver bump prerel "$VERSION")
echo "OUT: $OUT"
echo ::set-output name=tag_name::v$OUT
- 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: Pre-release ${{ steps.ready_tag.outputs.tag_name }}
- name: Release 📦
uses: softprops/action-gh-release@v1
if: ${{ github.event_name == 'workflow_dispatch' }}
with:
name: Nightly ${{ steps.tag_version.outputs.new_tag }}
tag_name: ${{ steps.tag_version.outputs.new_tag }}
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
# nightly:
# name: Release the nightly version of the package
# if: ${{ github.event_name == 'schedule' }}
# 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-nightly
# - 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
+23 -7
View File
@@ -11,14 +11,14 @@ Keep an eye on the [Wiki](https://deckbrew.xyz) for more information about Plugi
4. Under Miscellaneous, enable `CEF Remote Debugging`
5. Click on the `STEAM` button and select `Power` -> `Switch to Desktop`
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/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`
7. Open a terminal and paste the following command into it:
- For the latest pre-release:
- `curl -L https://github.com/SteamDeckHomebrew/decky-loader/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`
- `curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/main/dist/install_prerelease.sh | sh`
- [Wiki Link](https://deckbrew.xyz/en/loader-dev/development)
- For the legacy version (unsupported):
- `curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/legacy/dist/install_release.sh | sh`
7. Done! Reboot back into Gaming mode and enjoy your plugins!
### Install/Uninstall Plugins
@@ -28,7 +28,7 @@ Keep an eye on the [Wiki](https://deckbrew.xyz) for more information about Plugi
### Uninstall
- Open a terminal and paste the following command into it:
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/uninstall.sh | sh`
- `curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/main/dist/uninstall.sh | sh`
## Features
- Clean injecting and loading of one or more plugins
@@ -46,6 +46,22 @@ Keep an eye on the [Wiki](https://deckbrew.xyz) for more information about Plugi
- [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.)
### Getting Started
1. Clone the repository using the latest commit to main before starting your PR.
2. In your clone of the repository run these commands:
1. ``pnpm i``
2. ``pnpm run build``
3. If you are modifying the UI, these will need to be run before deploying the changes to your Deck.
4. Use the vscode tasks or ``deck.sh`` script to deploy your changes to your Deck to test them.
5. You will be testing your changes with the python script version, so you will need to build, deploy and reload each time.
Note: If you are recieveing build errors due to an out of date library, you should run this command inside of your repository:
```bash
pnpm update decky-frontend-lib --latest
```
Source control and deploying plugins are left to each respective contributor for the cloned repos in order to keep depedencies up to date.
## Credit
+48 -38
View File
@@ -1,19 +1,24 @@
from injector import get_tab
# Full imports
import json
# Partial imports
from aiohttp import ClientSession, web
from asyncio import get_event_loop
from concurrent.futures import ProcessPoolExecutor
from hashlib import sha256
from io import BytesIO
from logging import getLogger
from os import path, rename, listdir
from shutil import rmtree
from aiohttp import ClientSession, web
from io import BytesIO
from zipfile import ZipFile
from concurrent.futures import ProcessPoolExecutor
from asyncio import get_event_loop
from subprocess import call
from time import time
from hashlib import sha256
from subprocess import Popen
from zipfile import ZipFile
import json
# Local modules
from helpers import get_ssl_context, get_user, get_user_group
from injector import get_tab, inject_to_tab
import helpers
logger = getLogger("Browser")
class PluginInstallContext:
def __init__(self, artifact, name, version, hash) -> None:
@@ -23,9 +28,9 @@ class PluginInstallContext:
self.hash = hash
class PluginBrowser:
def __init__(self, plugin_path, server_instance) -> None:
self.log = getLogger("browser")
def __init__(self, plugin_path, server_instance, plugins) -> None:
self.plugin_path = plugin_path
self.plugins = plugins
self.install_requests = {}
server_instance.add_routes([
@@ -39,30 +44,40 @@ class PluginBrowser:
return False
zip_file = ZipFile(zip)
zip_file.extractall(self.plugin_path)
Popen(["chown", "-R", "deck:deck", self.plugin_path])
Popen(["chmod", "-R", "555", self.plugin_path])
code_chown = call(["chown", "-R", get_user()+":"+get_user_group(), self.plugin_path])
code_chmod = call(["chmod", "-R", "555", self.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})")
return False
return True
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)
try:
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)
if plugin['name'] == name:
return path.join(self.plugin_path, folder)
except:
logger.debug(f"skipping {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")
name = data.get("name", "undefined")
logger.info("uninstalling " + name)
logger.info(" at dir " + self.find_plugin_folder(name))
await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
if self.plugins[name]:
self.plugins[name].stop()
self.plugins.pop(name, None)
rmtree(self.find_plugin_folder(name))
except FileNotFoundError:
self.log.warning(f"Plugin {name} not installed, skipping uninstallation")
logger.warning(f"Plugin {name} not installed, skipping uninstallation")
return web.Response(text="Requested plugin uninstall")
@@ -70,31 +85,26 @@ class PluginBrowser:
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})")
logger.error(f"Plugin {name} not installed, skipping uninstallation")
logger.info(f"Installing {name} (Version: {version})")
async with ClientSession() as client:
self.log.debug(f"Fetching {artifact}")
res = await client.get(artifact, ssl=helpers.get_ssl_context())
logger.debug(f"Fetching {artifact}")
res = await client.get(artifact, ssl=get_ssl_context())
if res.status == 200:
self.log.debug("Got 200. Reading...")
logger.debug("Got 200. Reading...")
data = await res.read()
self.log.debug(f"Read {len(data)} bytes")
logger.debug(f"Read {len(data)} bytes")
res_zip = BytesIO(data)
with ProcessPoolExecutor() as executor:
self.log.debug("Unzipping...")
ret = await get_event_loop().run_in_executor(
executor,
self._unzip_to_plugin_dir,
res_zip,
name,
hash
)
logger.debug("Unzipping...")
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
if ret:
self.log.info(f"Installed {name} (Version: {version})")
logger.info(f"Installed {name} (Version: {version})")
await inject_to_tab("SP", "window.syncDeckyPlugins()")
else:
self.log.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
logger.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
else:
self.log.fatal(f"Could not fetch from URL. {await res.text()}")
logger.fatal(f"Could not fetch from URL. {await res.text()}")
async def install_plugin(self, request):
data = await request.post()
+78 -2
View File
@@ -1,7 +1,83 @@
import ssl
import certifi
import ssl
import uuid
import re
import subprocess
from aiohttp.web import middleware, Response
from subprocess import check_output
from time import sleep
REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service"
# global vars
csrf_token = str(uuid.uuid4())
ssl_ctx = ssl.create_default_context(cafile=certifi.where())
user = None
group = None
assets_regex = re.compile("^/plugins/.*/assets/.*")
def get_ssl_context():
return ssl_ctx
return ssl_ctx
def get_csrf_token():
return csrf_token
@middleware
async def csrf_middleware(request, handler):
if str(request.method) == "OPTIONS" or request.headers.get('Authentication') == csrf_token or str(request.rel_url) == "/auth/token" or str(request.rel_url).startswith("/plugins/load_main/") or str(request.rel_url).startswith("/static/") or str(request.rel_url).startswith("/legacy/") or str(request.rel_url).startswith("/steam_resource/") or assets_regex.match(str(request.rel_url)):
return await handler(request)
return Response(text='Forbidden', status='403')
# Get the user by checking for the first logged in user. As this is run
# by systemd at startup the process is likely to start before the user
# logs in, so we will wait here until they are available. Note that
# other methods such as getenv wont work as there was no $SUDO_USER to
# start the systemd service.
def set_user():
global user
cmd = "who | awk '{print $1}' | sort | head -1"
while user == None:
name = check_output(cmd, shell=True).decode().strip()
if name not in [None, '']:
user = name
sleep(0.1)
# Get the global user. get_user must be called first.
def get_user() -> str:
global user
if user == None:
raise ValueError("helpers.get_user method called before user variable was set. Run helpers.set_user first.")
return user
# Set the global user group. get_user must be called first
def set_user_group() -> str:
global group
global user
if user == None:
raise ValueError("helpers.set_user_dir method called before user variable was set. Run helpers.set_user first.")
if group == None:
group = check_output(["id", "-g", "-n", user]).decode().strip()
# Get the group of the global user. set_user_group must be called first.
def get_user_group() -> str:
global group
if group == None:
raise ValueError("helpers.get_user_group method called before group variable was set. Run helpers.set_user_group first.")
return group
async def is_systemd_unit_active(unit_name: str) -> bool:
res = subprocess.run(["systemctl", "is-active", unit_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return res.returncode == 0
async def stop_systemd_unit(unit_name: str) -> subprocess.CompletedProcess:
cmd = ["systemctl", "stop", unit_name]
return subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
async def start_systemd_unit(unit_name: str) -> subprocess.CompletedProcess:
cmd = ["systemctl", "start", unit_name]
return subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+13 -7
View File
@@ -5,6 +5,7 @@ from logging import debug, getLogger
from traceback import format_exc
from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientConnectorError
BASE_ADDRESS = "http://localhost:8080"
@@ -33,8 +34,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 +46,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):
@@ -62,17 +66,19 @@ async def get_tabs():
while True:
try:
res = await web.get(f"{BASE_ADDRESS}/json")
break
except:
except ClientConnectorError:
logger.debug("ClientConnectorError excepted.")
logger.debug("Steam isn't available yet. Wait for a moment...")
logger.debug(format_exc())
await sleep(5)
else:
break
if res.status == 200:
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()
+7 -1
View File
@@ -8,10 +8,13 @@ window.addEventListener("message", function(evt) {
}, false);
async function call_server_method(method_name, arg_object={}) {
const token = await fetch("http://127.0.0.1:1337/auth/token").then(r => r.text());
const response = await fetch(`http://127.0.0.1:1337/methods/${method_name}`, {
method: 'POST',
credentials: "include",
headers: {
'Content-Type': 'application/json',
Authentication: token
},
body: JSON.stringify(arg_object),
});
@@ -40,10 +43,13 @@ async function fetch_nocors(url, request={}) {
async function call_plugin_method(method_name, arg_object={}) {
if (plugin_name == undefined)
throw new Error("Plugin methods can only be called from inside plugins (duh)");
const token = await fetch("http://127.0.0.1:1337/auth/token").then(r => r.text());
const response = await fetch(`http://127.0.0.1:1337/plugins/${plugin_name}/methods/${method_name}`, {
method: 'POST',
credentials: "include",
headers: {
'Content-Type': 'application/json',
'Content-Type': 'application/json',
Authentication: token
},
body: JSON.stringify({
args: arg_object,
+48 -23
View File
@@ -1,10 +1,36 @@
from logging import DEBUG, INFO, basicConfig, getLogger
from os import getenv
# Full imports
import aiohttp_cors
# Partial imports
from aiohttp import ClientSession
from aiohttp.web import Application, run_app, static, get, Response
from aiohttp_jinja2 import setup as jinja_setup
from asyncio import get_event_loop, sleep
from json import dumps, loads
from logging import DEBUG, INFO, basicConfig, getLogger
from os import getenv, path
from subprocess import call
# local modules
from browser import PluginBrowser
from helpers import csrf_middleware, get_csrf_token, get_user, get_user_group, set_user, set_user_group, stop_systemd_unit, REMOTE_DEBUGGER_UNIT
from injector import inject_to_tab, tab_has_global_var
from loader import Loader
from updater import Updater
from utilities import Utilities
# Ensure USER and GROUP vars are set first.
# TODO: This isn't the best way to do this but supports the current
# implementation. All the config load and environment setting eventually be
# moved into init or a config/loader method.
set_user()
set_user_group()
USER = get_user()
GROUP = get_user_group()
HOME_PATH = "/home/"+USER
HOMEBREW_PATH = HOME_PATH+"/homebrew"
CONFIG = {
"plugin_path": getenv("PLUGIN_PATH", "/home/deck/homebrew/plugins"),
"plugin_path": getenv("PLUGIN_PATH", HOMEBREW_PATH+"/plugins"),
"chown_plugin_path": getenv("CHOWN_PLUGIN_PATH", "1") == "1",
"server_host": getenv("SERVER_HOST", "127.0.0.1"),
"server_port": int(getenv("SERVER_PORT", "1337")),
@@ -14,47 +40,41 @@ CONFIG = {
basicConfig(level=CONFIG["log_level"], format="[%(module)s][%(levelname)s]: %(message)s")
from asyncio import get_event_loop, sleep
from json import dumps, loads
from os import path
from subprocess import call
import aiohttp_cors
from aiohttp.web import Application, run_app, static
from aiohttp_jinja2 import setup as jinja_setup
from browser import PluginBrowser
from injector import inject_to_tab, tab_has_global_var
from loader import Loader
from utilities import Utilities
logger = getLogger("Main")
async def chown_plugin_dir(_):
code_chown = call(["chown", "-R", "deck:deck", CONFIG["plugin_path"]])
code_chown = call(["chown", "-R", USER+":"+GROUP, 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})")
def remote_debugging_allowed():
return path.exists(HOMEBREW_PATH + "/allow_remote_debugging")
class PluginManager:
def __init__(self) -> None:
self.loop = get_event_loop()
self.web_app = Application()
self.web_app.middlewares.append(csrf_middleware)
self.cors = aiohttp_cors.setup(self.web_app, defaults={
"https://steamloopback.host": aiohttp_cors.ResourceOptions(expose_headers="*",
allow_headers="*")
allow_headers="*", allow_credentials=True)
})
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)
self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.web_app, self.plugin_loader.plugins)
self.utilities = Utilities(self)
self.updater = Updater(self)
jinja_setup(self.web_app)
self.web_app.on_startup.append(self.inject_javascript)
if CONFIG["chown_plugin_path"] == True:
self.web_app.on_startup.append(chown_plugin_dir)
self.loop.create_task(self.loader_reinjector())
self.loop.create_task(self.load_plugins())
if not remote_debugging_allowed():
self.loop.create_task(stop_systemd_unit(REMOTE_DEBUGGER_UNIT))
self.loop.set_exception_handler(self.exception_handler)
self.web_app.add_routes([get("/auth/token", self.get_auth_token)])
for route in list(self.web_app.router.routes()):
self.cors.add(route)
self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), 'static'))])
@@ -65,6 +85,9 @@ class PluginManager:
return
loop.default_exception_handler(context)
async def get_auth_token(self, request):
return Response(text=get_csrf_token())
async def wait_for_server(self):
async with ClientSession() as web:
while True:
@@ -80,9 +103,11 @@ class PluginManager:
#await inject_to_tab("SP", "window.syncDeckyPlugins();")
async def loader_reinjector(self):
await sleep(2)
await self.inject_javascript()
while True:
await sleep(1)
if not await tab_has_global_var("SP", "DeckyPluginLoader"):
await sleep(5)
if not await tab_has_global_var("SP", "deckyHasLoaded"):
logger.info("Plugin loader isn't present in Steam anymore, reinjecting...")
await self.inject_javascript()
+20 -15
View File
@@ -78,12 +78,17 @@ class PluginWrapper:
async def _open_socket_if_not_exists(self):
if not self.reader:
while True:
retries = 0
while retries < 10:
try:
self.reader, self.writer = await open_unix_connection(self.socket_addr)
break
return True
except:
await sleep(0)
await sleep(2)
retries += 1
return False
else:
return True
def start(self):
if self.passive:
@@ -95,21 +100,21 @@ class PluginWrapper:
if self.passive:
return
async def _(self):
await self._open_socket_if_not_exists()
self.writer.write((dumps({"stop": True})+"\n").encode("utf-8"))
await self.writer.drain()
self.writer.close()
if await self._open_socket_if_not_exists():
self.writer.write((dumps({"stop": True})+"\n").encode("utf-8"))
await self.writer.drain()
self.writer.close()
get_event_loop().create_task(_(self))
async def execute_method(self, method_name, kwargs):
if self.passive:
raise RuntimeError("This plugin is passive (aka does not implement main.py)")
async with self.method_call_lock:
await self._open_socket_if_not_exists()
self.writer.write(
(dumps({"method": method_name, "args": kwargs})+"\n").encode("utf-8"))
await self.writer.drain()
res = loads((await self.reader.readline()).decode("utf-8"))
if not res["success"]:
raise Exception(res["res"])
return res["res"]
if await self._open_socket_if_not_exists():
self.writer.write(
(dumps({"method": method_name, "args": kwargs})+"\n").encode("utf-8"))
await self.writer.drain()
res = loads((await self.reader.readline()).decode("utf-8"))
if not res["success"]:
raise Exception(res["res"])
return res["res"]
+122
View File
@@ -0,0 +1,122 @@
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", "remote": self.remoteVer, "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"].find("-pre"), remoteVersions), None)
logger.info("Updated remote version information")
tab = await get_tab("SP")
await tab.evaluate_js(f"window.DeckyPluginLoader.notifyUpdates()", False, True, False)
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 * 6) # 6 hours
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"])
+17 -1
View File
@@ -5,6 +5,7 @@ from aiohttp import ClientSession, web
from injector import inject_to_tab
import helpers
import subprocess
class Utilities:
def __init__(self, context) -> None:
@@ -16,7 +17,10 @@ class Utilities:
"confirm_plugin_install": self.confirm_plugin_install,
"execute_in_tab": self.execute_in_tab,
"inject_css_into_tab": self.inject_css_into_tab,
"remove_css_from_tab": self.remove_css_from_tab
"remove_css_from_tab": self.remove_css_from_tab,
"allow_remote_debugging": self.allow_remote_debugging,
"disallow_remote_debugging": self.disallow_remote_debugging,
"remote_debugging_allowed": self.remote_debugging_allowed
}
if context:
@@ -133,3 +137,15 @@ class Utilities:
"success": False,
"result": e
}
async def remote_debugging_allowed(self):
return await helpers.is_systemd_unit_active(helpers.REMOTE_DEBUGGER_UNIT)
async def allow_remote_debugging(self):
await helpers.start_systemd_unit(helpers.REMOTE_DEBUGGER_UNIT)
return True
async def disallow_remote_debugging(self):
await helpers.stop_systemd_unit(helpers.REMOTE_DEBUGGER_UNIT)
return True
+8 -8
View File
@@ -4,12 +4,13 @@
echo "Installing Steam Deck Plugin Loader nightly..."
HOMEBREW_FOLDER=/home/deck/homebrew
USER_DIR="$(getent passwd $SUDO_USER | cut -d: -f6)"
HOMEBREW_FOLDER="${USER_DIR}/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
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services"
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins"
# Download latest nightly build and install it
rm -rf /tmp/plugin_loader
@@ -22,7 +23,7 @@ chmod +x ${HOMEBREW_FOLDER}/services/PluginLoader
systemctl --user stop plugin_loader 2> /dev/null
systemctl --user disable plugin_loader 2> /dev/null
rm -f /home/deck/.config/systemd/user/plugin_loader.service
rm -f ${USER_DIR}/.config/systemd/user/plugin_loader.service
systemctl stop plugin_loader 2> /dev/null
systemctl disable plugin_loader 2> /dev/null
@@ -37,10 +38,9 @@ Type=simple
User=root
Restart=always
ExecStart=/home/deck/homebrew/services/PluginLoader
WorkingDirectory=/home/deck/homebrew/services
Environment=PLUGIN_PATH=/home/deck/homebrew/plugins
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
WorkingDirectory=${HOMEBREW_FOLDER}/services
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
[Install]
WantedBy=multi-user.target
Vendored Executable → Regular
+12 -8
View File
@@ -4,18 +4,22 @@
echo "Installing Steam Deck Plugin Loader pre-release..."
HOMEBREW_FOLDER=/home/deck/homebrew
USER_DIR="$(getent passwd $SUDO_USER | cut -d: -f6)"
HOMEBREW_FOLDER="${USER_DIR}/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
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services"
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins"
# Download latest release and install it
DOWNLOADURL="$(curl -s 'https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases' | jq -r "first(.[] | select(.prerelease == "true"))" | jq -r ".assets[].browser_download_url")"
# printf "DOWNLOADURL=$DOWNLOADURL\n"
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
@@ -30,9 +34,9 @@ Description=SteamDeck Plugin Loader
Type=simple
User=root
Restart=always
ExecStart=/home/deck/homebrew/services/PluginLoader
WorkingDirectory=/home/deck/homebrew/services
Environment=PLUGIN_PATH=/home/deck/homebrew/plugins
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
WorkingDirectory=${HOMEBREW_FOLDER}/services
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
Environment=LOG_LEVEL=DEBUG
[Install]
WantedBy=multi-user.target
+12 -11
View File
@@ -4,33 +4,34 @@
echo "Installing Steam Deck Plugin Loader release..."
HOMEBREW_FOLDER=/home/deck/homebrew
USER_DIR="$(getent passwd $SUDO_USER | cut -d: -f6)"
HOMEBREW_FOLDER="${USER_DIR}/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
rm -rf "${HOMEBREW_FOLDER}/services"
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services"
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins"
# Download latest release and install it
curl -L https://github.com/SteamDeckHomebrew/PluginLoader/releases/latest/download/PluginLoader --output ${HOMEBREW_FOLDER}/services/PluginLoader
chmod +x ${HOMEBREW_FOLDER}/services/PluginLoader
curl -L https://github.com/SteamDeckHomebrew/PluginLoader/releases/latest/download/PluginLoader --output "${HOMEBREW_FOLDER}/services/PluginLoader"
chmod +x "${HOMEBREW_FOLDER}/services/PluginLoader"
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
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=/home/deck/homebrew/services/PluginLoader
WorkingDirectory=/home/deck/homebrew/services
Environment=PLUGIN_PATH=/home/deck/homebrew/plugins
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
WorkingDirectory=${HOMEBREW_FOLDER}/services
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
[Install]
WantedBy=multi-user.target
EOM
+8 -5
View File
@@ -1,17 +1,20 @@
#!/bin/sh
[ "$UID" -eq 0 ] || exec sudo "$0" "$@"
echo "Uninstalling Steam Deck Plugin Loader..."
HOMEBREW_FOLDER=/home/deck/homebrew
USER_DIR="$(getent passwd $SUDO_USER | cut -d: -f6)"
HOMEBREW_FOLDER="${USER_DIR}/homebrew"
# Disable and remove services
sudo systemctl disable --now plugin_loader.service > /dev/null
sudo rm -f /home/deck/.config/systemd/user/plugin_loader.service
sudo rm -f /etc/systemd/system/plugin_loader.service
sudo rm -f "${USER_DIR}/.config/systemd/user/plugin_loader.service"
sudo rm -f "/etc/systemd/system/plugin_loader.service"
# Remove temporary folder if it exists from the install process
rm -rf /tmp/plugin_loader
rm -rf "/tmp/plugin_loader"
# Cleanup services folder
sudo rm ${HOMEBREW_FOLDER}/services/PluginLoader
sudo rm "${HOMEBREW_FOLDER}/services/PluginLoader"
+2 -2
View File
@@ -26,7 +26,7 @@
"prettier-plugin-import-sort": "^0.0.7",
"react": "16.14.0",
"react-dom": "16.14.0",
"rollup": "^2.75.7",
"rollup": "^2.76.0",
"tslib": "^2.4.0",
"typescript": "^4.7.4"
},
@@ -37,7 +37,7 @@
}
},
"dependencies": {
"decky-frontend-lib": "^1.0.2",
"decky-frontend-lib": "^1.7.5",
"react-icons": "^4.4.0"
}
}
+106 -100
View File
@@ -9,7 +9,7 @@ specifiers:
'@types/react': 16.14.0
'@types/react-router': 5.1.18
'@types/webpack': ^5.28.0
decky-frontend-lib: ^1.0.2
decky-frontend-lib: ^1.7.5
husky: ^8.0.1
import-sort-style-module: ^6.0.0
inquirer: ^8.2.4
@@ -18,20 +18,20 @@ specifiers:
react: 16.14.0
react-dom: 16.14.0
react-icons: ^4.4.0
rollup: ^2.75.7
rollup: ^2.76.0
tslib: ^2.4.0
typescript: ^4.7.4
dependencies:
decky-frontend-lib: 1.0.2
decky-frontend-lib: 1.7.5
react-icons: 4.4.0_react@16.14.0
devDependencies:
'@rollup/plugin-commonjs': 21.1.0_rollup@2.75.7
'@rollup/plugin-json': 4.1.0_rollup@2.75.7
'@rollup/plugin-node-resolve': 13.3.0_rollup@2.75.7
'@rollup/plugin-replace': 4.0.0_rollup@2.75.7
'@rollup/plugin-typescript': 8.3.3_g4qkabtmybowem44p7ts7jnbqm
'@rollup/plugin-commonjs': 21.1.0_rollup@2.76.0
'@rollup/plugin-json': 4.1.0_rollup@2.76.0
'@rollup/plugin-node-resolve': 13.3.0_rollup@2.76.0
'@rollup/plugin-replace': 4.0.0_rollup@2.76.0
'@rollup/plugin-typescript': 8.3.3_mrkdcqv53wzt2ybukxlrvz47fu
'@types/react': 16.14.0
'@types/react-router': 5.1.18
'@types/webpack': 5.28.0
@@ -42,7 +42,7 @@ devDependencies:
prettier-plugin-import-sort: 0.0.7_prettier@2.7.1
react: 16.14.0
react-dom: 16.14.0_react@16.14.0
rollup: 2.75.7
rollup: 2.76.0
tslib: 2.4.0
typescript: 4.7.4
@@ -63,8 +63,8 @@ packages:
'@babel/highlight': 7.18.6
dev: true
/@babel/compat-data/7.18.6:
resolution: {integrity: sha512-tzulrgDT0QD6U7BJ4TKVk2SDDg7wlP39P9yAx1RfLy7vP/7rsDRlWVfbWxElslu56+r7QOhB2NSDsabYYruoZQ==}
/@babel/compat-data/7.18.8:
resolution: {integrity: sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ==}
engines: {node: '>=6.9.0'}
dev: true
@@ -76,12 +76,12 @@ packages:
'@babel/code-frame': 7.18.6
'@babel/generator': 7.18.7
'@babel/helper-compilation-targets': 7.18.6_@babel+core@7.18.6
'@babel/helper-module-transforms': 7.18.6
'@babel/helper-module-transforms': 7.18.8
'@babel/helpers': 7.18.6
'@babel/parser': 7.18.6
'@babel/parser': 7.18.8
'@babel/template': 7.18.6
'@babel/traverse': 7.18.6
'@babel/types': 7.18.7
'@babel/traverse': 7.18.8
'@babel/types': 7.18.8
convert-source-map: 1.8.0
debug: 4.3.4
gensync: 1.0.0-beta.2
@@ -95,7 +95,7 @@ packages:
resolution: {integrity: sha512-shck+7VLlY72a2w9c3zYWuE1pwOKEiQHV7GTUbSnhyl5eu3i04t30tBY82ZRWrDfo3gkakCFtevExnxbkf2a3A==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.18.7
'@babel/types': 7.18.8
'@jridgewell/gen-mapping': 0.3.2
jsesc: 2.5.2
dev: true
@@ -106,10 +106,10 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
'@babel/compat-data': 7.18.6
'@babel/compat-data': 7.18.8
'@babel/core': 7.18.6
'@babel/helper-validator-option': 7.18.6
browserslist: 4.21.1
browserslist: 4.21.2
semver: 6.3.0
dev: true
@@ -123,25 +123,25 @@ packages:
engines: {node: '>=6.9.0'}
dependencies:
'@babel/template': 7.18.6
'@babel/types': 7.18.7
'@babel/types': 7.18.8
dev: true
/@babel/helper-hoist-variables/7.18.6:
resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.18.7
'@babel/types': 7.18.8
dev: true
/@babel/helper-module-imports/7.18.6:
resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.18.7
'@babel/types': 7.18.8
dev: true
/@babel/helper-module-transforms/7.18.6:
resolution: {integrity: sha512-L//phhB4al5uucwzlimruukHB3jRd5JGClwRMD/ROrVjXfLqovYnvQrK/JK36WYyVwGGO7OD3kMyVTjx+WVPhw==}
/@babel/helper-module-transforms/7.18.8:
resolution: {integrity: sha512-che3jvZwIcZxrwh63VfnFTUzcAM9v/lznYkkRxIBGMPt1SudOKHAEec0SIRCfiuIzTcF7VGj/CaTT6gY4eWxvA==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/helper-environment-visitor': 7.18.6
@@ -150,8 +150,8 @@ packages:
'@babel/helper-split-export-declaration': 7.18.6
'@babel/helper-validator-identifier': 7.18.6
'@babel/template': 7.18.6
'@babel/traverse': 7.18.6
'@babel/types': 7.18.7
'@babel/traverse': 7.18.8
'@babel/types': 7.18.8
transitivePeerDependencies:
- supports-color
dev: true
@@ -160,14 +160,14 @@ packages:
resolution: {integrity: sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.18.7
'@babel/types': 7.18.8
dev: true
/@babel/helper-split-export-declaration/7.18.6:
resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.18.7
'@babel/types': 7.18.8
dev: true
/@babel/helper-validator-identifier/7.18.6:
@@ -185,8 +185,8 @@ packages:
engines: {node: '>=6.9.0'}
dependencies:
'@babel/template': 7.18.6
'@babel/traverse': 7.18.6
'@babel/types': 7.18.7
'@babel/traverse': 7.18.8
'@babel/types': 7.18.8
transitivePeerDependencies:
- supports-color
dev: true
@@ -200,12 +200,12 @@ packages:
js-tokens: 4.0.0
dev: true
/@babel/parser/7.18.6:
resolution: {integrity: sha512-uQVSa9jJUe/G/304lXspfWVpKpK4euFLgGiMQFOCpM/bgcAdeoHwi/OQz23O9GK2osz26ZiXRRV9aV+Yl1O8tw==}
/@babel/parser/7.18.8:
resolution: {integrity: sha512-RSKRfYX20dyH+elbJK2uqAkVyucL+xXzhqlMD5/ZXx+dAAwpyB7HsvnHe/ZUGOF+xLr5Wx9/JoXVTj6BQE2/oA==}
engines: {node: '>=6.0.0'}
hasBin: true
dependencies:
'@babel/types': 7.18.7
'@babel/types': 7.18.8
dev: true
/@babel/template/7.18.6:
@@ -213,12 +213,12 @@ packages:
engines: {node: '>=6.9.0'}
dependencies:
'@babel/code-frame': 7.18.6
'@babel/parser': 7.18.6
'@babel/types': 7.18.7
'@babel/parser': 7.18.8
'@babel/types': 7.18.8
dev: true
/@babel/traverse/7.18.6:
resolution: {integrity: sha512-zS/OKyqmD7lslOtFqbscH6gMLFYOfG1YPqCKfAW5KrTeolKqvB8UelR49Fpr6y93kYkW2Ik00mT1LOGiAGvizw==}
/@babel/traverse/7.18.8:
resolution: {integrity: sha512-UNg/AcSySJYR/+mIcJQDCv00T+AqRO7j/ZEJLzpaYtgM48rMg5MnkJgyNqkzo88+p4tfRvZJCEiwwfG6h4jkRg==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/code-frame': 7.18.6
@@ -227,16 +227,16 @@ packages:
'@babel/helper-function-name': 7.18.6
'@babel/helper-hoist-variables': 7.18.6
'@babel/helper-split-export-declaration': 7.18.6
'@babel/parser': 7.18.6
'@babel/types': 7.18.7
'@babel/parser': 7.18.8
'@babel/types': 7.18.8
debug: 4.3.4
globals: 11.12.0
transitivePeerDependencies:
- supports-color
dev: true
/@babel/types/7.18.7:
resolution: {integrity: sha512-QG3yxTcTIBoAcQmkCs+wAPYZhu7Dk9rXKacINfNbdJDNERTbLQbHGyVG8q/YGMPeCJRIhSY0+fTc5+xuh6WPSQ==}
/@babel/types/7.18.8:
resolution: {integrity: sha512-qwpdsmraq0aJ3osLJRApsc2ouSJCdnMeZwB0DhbtHAtRpZNZCdlbRnHIgcRKzdE1g0iOGg644fzjOBcdOz9cPw==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/helper-validator-identifier': 7.18.6
@@ -260,8 +260,8 @@ packages:
'@jridgewell/trace-mapping': 0.3.14
dev: true
/@jridgewell/resolve-uri/3.0.8:
resolution: {integrity: sha512-YK5G9LaddzGbcucK4c8h5tWFmMPBvRZ/uyWmN1/SbBdIvqGUdWGkJ5BAaccgs6XbzVLsqbPJrBSFwKv3kT9i7w==}
/@jridgewell/resolve-uri/3.1.0:
resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==}
engines: {node: '>=6.0.0'}
dev: true
@@ -284,61 +284,61 @@ packages:
/@jridgewell/trace-mapping/0.3.14:
resolution: {integrity: sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==}
dependencies:
'@jridgewell/resolve-uri': 3.0.8
'@jridgewell/resolve-uri': 3.1.0
'@jridgewell/sourcemap-codec': 1.4.14
dev: true
/@rollup/plugin-commonjs/21.1.0_rollup@2.75.7:
/@rollup/plugin-commonjs/21.1.0_rollup@2.76.0:
resolution: {integrity: sha512-6ZtHx3VHIp2ReNNDxHjuUml6ur+WcQ28N1yHgCQwsbNkQg2suhxGMDQGJOn/KuDxKtd1xuZP5xSTwBA4GQ8hbA==}
engines: {node: '>= 8.0.0'}
peerDependencies:
rollup: ^2.38.3
dependencies:
'@rollup/pluginutils': 3.1.0_rollup@2.75.7
'@rollup/pluginutils': 3.1.0_rollup@2.76.0
commondir: 1.0.1
estree-walker: 2.0.2
glob: 7.2.3
is-reference: 1.2.1
magic-string: 0.25.9
resolve: 1.22.1
rollup: 2.75.7
rollup: 2.76.0
dev: true
/@rollup/plugin-json/4.1.0_rollup@2.75.7:
/@rollup/plugin-json/4.1.0_rollup@2.76.0:
resolution: {integrity: sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==}
peerDependencies:
rollup: ^1.20.0 || ^2.0.0
dependencies:
'@rollup/pluginutils': 3.1.0_rollup@2.75.7
rollup: 2.75.7
'@rollup/pluginutils': 3.1.0_rollup@2.76.0
rollup: 2.76.0
dev: true
/@rollup/plugin-node-resolve/13.3.0_rollup@2.75.7:
/@rollup/plugin-node-resolve/13.3.0_rollup@2.76.0:
resolution: {integrity: sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw==}
engines: {node: '>= 10.0.0'}
peerDependencies:
rollup: ^2.42.0
dependencies:
'@rollup/pluginutils': 3.1.0_rollup@2.75.7
'@rollup/pluginutils': 3.1.0_rollup@2.76.0
'@types/resolve': 1.17.1
deepmerge: 4.2.2
is-builtin-module: 3.1.0
is-module: 1.0.0
resolve: 1.22.1
rollup: 2.75.7
rollup: 2.76.0
dev: true
/@rollup/plugin-replace/4.0.0_rollup@2.75.7:
/@rollup/plugin-replace/4.0.0_rollup@2.76.0:
resolution: {integrity: sha512-+rumQFiaNac9y64OHtkHGmdjm7us9bo1PlbgQfdihQtuNxzjpaB064HbRnewUOggLQxVCCyINfStkgmBeQpv1g==}
peerDependencies:
rollup: ^1.20.0 || ^2.0.0
dependencies:
'@rollup/pluginutils': 3.1.0_rollup@2.75.7
'@rollup/pluginutils': 3.1.0_rollup@2.76.0
magic-string: 0.25.9
rollup: 2.75.7
rollup: 2.76.0
dev: true
/@rollup/plugin-typescript/8.3.3_g4qkabtmybowem44p7ts7jnbqm:
/@rollup/plugin-typescript/8.3.3_mrkdcqv53wzt2ybukxlrvz47fu:
resolution: {integrity: sha512-55L9SyiYu3r/JtqdjhwcwaECXP7JeJ9h1Sg1VWRJKIutla2MdZQodTgcCNybXLMCnqpNLEhS2vGENww98L1npg==}
engines: {node: '>=8.0.0'}
peerDependencies:
@@ -349,14 +349,14 @@ packages:
tslib:
optional: true
dependencies:
'@rollup/pluginutils': 3.1.0_rollup@2.75.7
'@rollup/pluginutils': 3.1.0_rollup@2.76.0
resolve: 1.22.1
rollup: 2.75.7
rollup: 2.76.0
tslib: 2.4.0
typescript: 4.7.4
dev: true
/@rollup/pluginutils/3.1.0_rollup@2.75.7:
/@rollup/pluginutils/3.1.0_rollup@2.76.0:
resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==}
engines: {node: '>= 8.0.0'}
peerDependencies:
@@ -365,18 +365,18 @@ packages:
'@types/estree': 0.0.39
estree-walker: 1.0.1
picomatch: 2.3.1
rollup: 2.75.7
rollup: 2.76.0
dev: true
/@types/eslint-scope/3.7.4:
resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==}
dependencies:
'@types/eslint': 8.4.3
'@types/eslint': 8.4.5
'@types/estree': 0.0.51
dev: true
/@types/eslint/8.4.3:
resolution: {integrity: sha512-YP1S7YJRMPs+7KZKDb9G63n8YejIwW9BALq7a5j2+H4yl6iOv9CB29edho+cuFRrvmJbbaH2yiVChKLJVysDGw==}
/@types/eslint/8.4.5:
resolution: {integrity: sha512-dhsC09y1gpJWnK+Ff4SGvCuSnk9DaU0BJZSzOwa6GVSg65XtTugLBITDAAzRU5duGBoXBHpdR/9jHGxJjNflJQ==}
dependencies:
'@types/estree': 0.0.51
'@types/json-schema': 7.0.11
@@ -390,8 +390,8 @@ packages:
resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==}
dev: true
/@types/estree/0.0.52:
resolution: {integrity: sha512-BZWrtCU0bMVAIliIV+HJO1f1PR41M7NKjfxrFJwwhKI1KwhwOxYw1SXg9ao+CIMt774nFuGiG6eU+udtbEI9oQ==}
/@types/estree/1.0.0:
resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==}
dev: true
/@types/history/4.7.11:
@@ -402,8 +402,8 @@ packages:
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
dev: true
/@types/node/18.0.0:
resolution: {integrity: sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==}
/@types/node/18.0.4:
resolution: {integrity: sha512-M0+G6V0Y4YV8cqzHssZpaNCqvYwlCiulmm0PwpNLF55r/+cT8Ol42CHRU1SEaYFH2rTwiiE1aYg/2g2rrtGdPA==}
dev: true
/@types/prop-types/15.7.5:
@@ -427,13 +427,13 @@ packages:
/@types/resolve/1.17.1:
resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
dependencies:
'@types/node': 18.0.0
'@types/node': 18.0.4
dev: true
/@types/webpack/5.28.0:
resolution: {integrity: sha512-8cP0CzcxUiFuA9xGJkfeVpqmWTk9nx6CWwamRGCj95ph1SmlRRk9KlCZ6avhCbZd4L68LvYT6l1kpdEnQXrF8w==}
dependencies:
'@types/node': 18.0.0
'@types/node': 18.0.4
tapable: 2.2.1
webpack: 5.73.0
transitivePeerDependencies:
@@ -643,15 +643,15 @@ packages:
concat-map: 0.0.1
dev: true
/browserslist/4.21.1:
resolution: {integrity: sha512-Nq8MFCSrnJXSc88yliwlzQe3qNe3VntIjhsArW9IJOEPSHNx23FalwApUVbzAWABLhYJJ7y8AynWI/XM8OdfjQ==}
/browserslist/4.21.2:
resolution: {integrity: sha512-MonuOgAtUB46uP5CezYbRaYKBNt2LxP0yX+Pmj4LkcDFGkn9Cbpi83d9sCjwQDErXsIJSzY5oKGDbgOlF/LPAA==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
dependencies:
caniuse-lite: 1.0.30001361
electron-to-chromium: 1.4.174
node-releases: 2.0.5
update-browserslist-db: 1.0.4_browserslist@4.21.1
caniuse-lite: 1.0.30001366
electron-to-chromium: 1.4.189
node-releases: 2.0.6
update-browserslist-db: 1.0.4_browserslist@4.21.2
dev: true
/buffer-from/1.1.2:
@@ -689,8 +689,8 @@ packages:
engines: {node: '>=4'}
dev: true
/caniuse-lite/1.0.30001361:
resolution: {integrity: sha512-ybhCrjNtkFji1/Wto6SSJKkWk6kZgVQsDq5QI83SafsF6FXv2JB4df9eEdH6g8sdGgqTXrFLjAxqBGgYoU3azQ==}
/caniuse-lite/1.0.30001366:
resolution: {integrity: sha512-yy7XLWCubDobokgzudpkKux8e0UOOnLHE6mlNJBzT3lZJz6s5atSEzjoL+fsCPkI0G8MP5uVdDx1ur/fXEWkZA==}
dev: true
/chalk/2.4.2:
@@ -771,7 +771,7 @@ packages:
dev: true
/concat-map/0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
dev: true
/convert-source-map/1.8.0:
@@ -806,8 +806,10 @@ packages:
ms: 2.1.2
dev: true
/decky-frontend-lib/1.0.2:
resolution: {integrity: sha512-l2Fq1oKkZdi2W4Qq+EXWgwOynWgANLTSFHcHrbJwCimTJ75uPlvrtsulnh5PlIovydM6iC+NyjAbW/qsVXWeLg==}
/decky-frontend-lib/1.7.5:
resolution: {integrity: sha512-1OX/Ix9W76gF0NJjfm0k/01LYPmC2k/k+k/qqH8JJPlPHh5+W5P8ZG2T8m5wKsqoP7jx2W3k7RNZBh9vAqFoFw==}
dependencies:
minimist: 1.2.6
dev: false
/deepmerge/4.2.2:
@@ -826,8 +828,8 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/electron-to-chromium/1.4.174:
resolution: {integrity: sha512-JER+w+9MV2MBVFOXxP036bLlNOnzbYAWrWU8sNUwoOO69T3w4564WhM5H5atd8VVS8U4vpi0i0kdoYzm1NPQgQ==}
/electron-to-chromium/1.4.189:
resolution: {integrity: sha512-dQ6Zn4ll2NofGtxPXaDfY2laIa6NyCQdqXYHdwH90GJQW0LpJJib0ZU/ERtbb0XkBEmUD2eJtagbOie3pdMiPg==}
dev: true
/emoji-regex/8.0.0:
@@ -1038,9 +1040,9 @@ packages:
resolution: {integrity: sha512-NyShTiNhTh4Vy7kJUVe6CuvOaQAzzfSIT72wtp3CzGjz8bHjNj59DCAjncuviicmDOgVAgmLuSh1WMcLYAMWGg==}
dependencies:
'@babel/core': 7.18.6
'@babel/parser': 7.18.6
'@babel/traverse': 7.18.6
'@babel/types': 7.18.7
'@babel/parser': 7.18.8
'@babel/traverse': 7.18.8
'@babel/types': 7.18.8
find-line-column: 0.5.2
transitivePeerDependencies:
- supports-color
@@ -1099,7 +1101,7 @@ packages:
mute-stream: 0.0.8
ora: 5.4.1
run-async: 2.4.1
rxjs: 7.5.5
rxjs: 7.5.6
string-width: 4.2.3
strip-ansi: 6.0.1
through: 2.3.8
@@ -1145,7 +1147,7 @@ packages:
/is-reference/1.2.1:
resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==}
dependencies:
'@types/estree': 0.0.52
'@types/estree': 1.0.0
dev: true
/is-unicode-supported/0.1.0:
@@ -1157,7 +1159,7 @@ packages:
resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
engines: {node: '>= 10.13.0'}
dependencies:
'@types/node': 18.0.0
'@types/node': 18.0.4
merge-stream: 2.0.0
supports-color: 8.1.1
dev: true
@@ -1253,6 +1255,10 @@ packages:
brace-expansion: 1.1.11
dev: true
/minimist/1.2.6:
resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==}
dev: false
/ms/2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: true
@@ -1265,8 +1271,8 @@ packages:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
dev: true
/node-releases/2.0.5:
resolution: {integrity: sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q==}
/node-releases/2.0.6:
resolution: {integrity: sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==}
dev: true
/object-assign/4.1.1:
@@ -1437,8 +1443,8 @@ packages:
signal-exit: 3.0.7
dev: true
/rollup/2.75.7:
resolution: {integrity: sha512-VSE1iy0eaAYNCxEXaleThdFXqZJ42qDBatAwrfnPlENEZ8erQ+0LYX4JXOLPceWfZpV1VtZwZ3dFCuOZiSyFtQ==}
/rollup/2.76.0:
resolution: {integrity: sha512-9jwRIEY1jOzKLj3nsY/yot41r19ITdQrhs+q3ggNWhr9TQgduHqANvPpS32RNpzGklJu3G1AJfvlZLi/6wFgWA==}
engines: {node: '>=10.0.0'}
hasBin: true
optionalDependencies:
@@ -1450,8 +1456,8 @@ packages:
engines: {node: '>=0.12.0'}
dev: true
/rxjs/7.5.5:
resolution: {integrity: sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==}
/rxjs/7.5.6:
resolution: {integrity: sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==}
dependencies:
tslib: 2.4.0
dev: true
@@ -1592,12 +1598,12 @@ packages:
jest-worker: 27.5.1
schema-utils: 3.1.1
serialize-javascript: 6.0.0
terser: 5.14.1
terser: 5.14.2
webpack: 5.73.0
dev: true
/terser/5.14.1:
resolution: {integrity: sha512-+ahUAE+iheqBTDxXhTisdA8hgvbEG1hHOQ9xmNjeUJSoi6DU/gMrKNcfZjHkyY6Alnuyc+ikYJaxxfHkT3+WuQ==}
/terser/5.14.2:
resolution: {integrity: sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==}
engines: {node: '>=10'}
hasBin: true
dependencies:
@@ -1644,13 +1650,13 @@ packages:
hasBin: true
dev: true
/update-browserslist-db/1.0.4_browserslist@4.21.1:
/update-browserslist-db/1.0.4_browserslist@4.21.2:
resolution: {integrity: sha512-jnmO2BEGUjsMOe/Fg9u0oczOe/ppIDZPebzccl1yDWGLFP16Pa1/RM5wEoKYPG2zstNcDuAStejyxsOuKINdGA==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
dependencies:
browserslist: 4.21.1
browserslist: 4.21.2
escalade: 3.1.1
picocolors: 1.0.0
dev: true
@@ -1701,7 +1707,7 @@ packages:
'@webassemblyjs/wasm-parser': 1.11.1
acorn: 8.7.1
acorn-import-assertions: 1.8.0_acorn@8.7.1
browserslist: 4.21.1
browserslist: 4.21.2
chrome-trace-event: 1.0.3
enhanced-resolve: 5.10.0
es-module-lexer: 0.9.3
+34 -5
View File
@@ -6,17 +6,21 @@ export interface RouterEntry {
component: ComponentType;
}
export type RoutePatch = (route: RouteProps) => RouteProps;
interface PublicDeckyRouterState {
routes: Map<string, RouterEntry>;
routePatches: Map<string, Set<RoutePatch>>;
}
export class DeckyRouterState {
private _routes = new Map<string, RouterEntry>();
private _routePatches = new Map<string, Set<RoutePatch>>();
public eventBus = new EventTarget();
publicState(): PublicDeckyRouterState {
return { routes: this._routes };
return { routes: this._routes, routePatches: this._routePatches };
}
addRoute(path: string, component: RouterEntry['component'], props: RouterEntry['props'] = {}) {
@@ -24,6 +28,26 @@ export class DeckyRouterState {
this.notifyUpdate();
}
addPatch(path: string, patch: RoutePatch) {
let patchList = this._routePatches.get(path);
if (!patchList) {
patchList = new Set();
this._routePatches.set(path, patchList);
}
patchList.add(patch);
this.notifyUpdate();
return patch;
}
removePatch(path: string, patch: RoutePatch) {
const patchList = this._routePatches.get(path);
patchList?.delete(patch);
if (patchList?.size == 0) {
this._routePatches.delete(path);
}
this.notifyUpdate();
}
removeRoute(path: string) {
this._routes.delete(path);
this.notifyUpdate();
@@ -36,6 +60,8 @@ export class DeckyRouterState {
interface DeckyRouterStateContext extends PublicDeckyRouterState {
addRoute(path: string, component: RouterEntry['component'], props: RouterEntry['props']): void;
addPatch(path: string, patch: RoutePatch): RoutePatch;
removePatch(path: string, patch: RoutePatch): void;
removeRoute(path: string): void;
}
@@ -62,12 +88,15 @@ export const DeckyRouterStateContextProvider: FC<Props> = ({ children, deckyRout
return () => deckyRouterState.eventBus.removeEventListener('update', onUpdate);
}, []);
const addRoute = (path: string, component: RouterEntry['component'], props: RouterEntry['props'] = {}) =>
deckyRouterState.addRoute(path, component, props);
const removeRoute = (path: string) => deckyRouterState.removeRoute(path);
const addRoute = deckyRouterState.addRoute.bind(deckyRouterState);
const addPatch = deckyRouterState.addPatch.bind(deckyRouterState);
const removePatch = deckyRouterState.removePatch.bind(deckyRouterState);
const removeRoute = deckyRouterState.removeRoute.bind(deckyRouterState);
return (
<DeckyRouterStateContext.Provider value={{ ...publicDeckyRouterState, addRoute, removeRoute }}>
<DeckyRouterStateContext.Provider
value={{ ...publicDeckyRouterState, addRoute, addPatch, removePatch, removeRoute }}
>
{children}
</DeckyRouterStateContext.Provider>
);
+32 -16
View File
@@ -1,4 +1,11 @@
import { ButtonItem, PanelSection, PanelSectionRow } from 'decky-frontend-lib';
import {
ButtonItem,
PanelSection,
PanelSectionRow,
joinClassNames,
scrollClasses,
staticClasses,
} from 'decky-frontend-lib';
import { VFC } from 'react';
import { useDeckyState } from './DeckyState';
@@ -7,24 +14,33 @@ const PluginView: VFC = () => {
const { plugins, activePlugin, setActivePlugin } = useDeckyState();
if (activePlugin) {
return <div style={{ height: '100%' }}>{activePlugin.content}</div>;
return (
<div
className={joinClassNames(staticClasses.TabGroupPanel, scrollClasses.ScrollPanel, scrollClasses.ScrollY)}
style={{ height: '100%' }}
>
{activePlugin.content}
</div>
);
}
return (
<PanelSection>
{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>
<div className={joinClassNames(staticClasses.TabGroupPanel, scrollClasses.ScrollPanel, scrollClasses.ScrollY)}>
<PanelSection>
{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>
</div>
);
};
-1
View File
@@ -7,7 +7,6 @@ import { useDeckyState } from './DeckyState';
const titleStyles: CSSProperties = {
display: 'flex',
paddingTop: '3px',
paddingBottom: '14px',
paddingRight: '16px',
boxShadow: 'unset',
};
+54
View File
@@ -0,0 +1,54 @@
import { ToastData, findModule, joinClassNames } from 'decky-frontend-lib';
import { FunctionComponent } from 'react';
interface ToastProps {
toast: {
data: ToastData;
nToastDurationMS: number;
};
}
const toastClasses = findModule((mod) => {
if (typeof mod !== 'object') return false;
if (mod.ToastPlaceholder) {
return true;
}
return false;
});
const templateClasses = findModule((mod) => {
if (typeof mod !== 'object') return false;
if (mod.ShortTemplate) {
return true;
}
return false;
});
const Toast: FunctionComponent<ToastProps> = ({ toast }) => {
return (
<div
style={{ '--toast-duration': `${toast.nToastDurationMS}ms` } as React.CSSProperties}
className={joinClassNames(toastClasses.ToastPopup, toastClasses.toastEnter)}
>
<div
onClick={toast.data.onClick}
className={joinClassNames(templateClasses.ShortTemplate, toast.data.className || '')}
>
{toast.data.logo && <div className={templateClasses.StandardLogoDimensions}>{toast.data.logo}</div>}
<div className={joinClassNames(templateClasses.Content, toast.data.contentClassName || '')}>
<div className={templateClasses.Header}>
{toast.data.icon && <div className={templateClasses.Icon}>{toast.data.icon}</div>}
<div className={templateClasses.Title}>{toast.data.title}</div>
</div>
<div className={templateClasses.Body}>{toast.data.body}</div>
</div>
</div>
</div>
);
};
export default Toast;
@@ -0,0 +1,43 @@
import { ModalRoot, QuickAccessTab, Router, Spinner, sleep, staticClasses } from 'decky-frontend-lib';
import { FC, useState } from 'react';
interface PluginInstallModalProps {
artifact: string;
version: string;
hash: string;
// reinstall: boolean;
onOK(): void;
onCancel(): void;
closeModal?(): void;
}
const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, hash, onOK, onCancel, closeModal }) => {
const [loading, setLoading] = useState<boolean>(false);
return (
<ModalRoot
bOKDisabled={loading}
closeModal={closeModal}
onOK={async () => {
setLoading(true);
await onOK();
Router.NavigateBackOrOpenMenu();
await sleep(250);
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 1000);
}}
onCancel={async () => {
await onCancel();
}}
>
<div className={staticClasses.Title} style={{ flexDirection: 'column' }}>
{hash == 'False' ? <h3 style={{ color: 'red' }}>!!!!NO HASH PROVIDED!!!!</h3> : null}
<div style={{ flexDirection: 'row' }}>
{loading && <Spinner style={{ width: '20px' }} />} {loading ? 'Installing' : 'Install'} {artifact}
{version ? ' version ' + version : null}
{!loading && '?'}
</div>
</div>
</ModalRoot>
);
};
export default PluginInstallModal;
+2 -2
View File
@@ -1,7 +1,7 @@
import { SidebarNavigation } from 'decky-frontend-lib';
import GeneralSettings from './pages/GeneralSettings';
import PluginList from './pages/PluginList';
import GeneralSettings from './pages/general';
import PluginList from './pages/plugin_list';
export default function SettingsPage() {
return (
@@ -0,0 +1,34 @@
import { Field, ToggleField } from 'decky-frontend-lib';
import { useEffect, useState } from 'react';
import { FaBug } from 'react-icons/fa';
export default function RemoteDebuggingSettings() {
const [allowRemoteDebugging, setAllowRemoteDebugging] = useState<boolean>(false);
useEffect(() => {
(async () => {
const res = (await window.DeckyPluginLoader.callServerMethod('remote_debugging_allowed')) as { result: boolean };
setAllowRemoteDebugging(res.result);
})();
}, []);
return (
<Field
label="Allow Remote CEF Debugging"
description={
<span style={{ whiteSpace: 'pre-line' }}>
Allow unauthenticated access to the CEF debugger to anyone in your network
</span>
}
icon={<FaBug style={{ display: 'block' }} />}
>
<ToggleField
checked={allowRemoteDebugging}
onChange={(toggleValue) => {
setAllowRemoteDebugging(toggleValue);
if (toggleValue) window.DeckyPluginLoader.callServerMethod('allow_remote_debugging');
else window.DeckyPluginLoader.callServerMethod('disallow_remote_debugging');
}}
/>
</Field>
);
}
@@ -0,0 +1,81 @@
import { DialogButton, Field, ProgressBarWithInfo, Spinner } from 'decky-frontend-lib';
import { useEffect, useState } from 'react';
import { FaArrowDown } from 'react-icons/fa';
import { VerInfo, callUpdaterMethod, finishUpdate } from '../../../../updater';
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>
);
}
@@ -2,7 +2,9 @@ import { DialogButton, Field, TextField } from 'decky-frontend-lib';
import { useState } from 'react';
import { FaShapes } from 'react-icons/fa';
import { installFromURL } from '../../store/Store';
import { installFromURL } from '../../../store/Store';
import RemoteDebuggingSettings from './RemoteDebugging';
import UpdaterSettings from './Updater';
export default function GeneralSettings() {
const [pluginURL, setPluginURL] = useState('');
@@ -18,12 +20,16 @@ export default function GeneralSettings() {
onChange={(e) => setChecked(e)}
/>
</Field> */}
<UpdaterSettings />
<RemoteDebuggingSettings />
<Field
label="Manual plugin install"
description={<TextField label={'URL'} value={pluginURL} onChange={(e) => setPluginURL(e?.target.value)} />}
icon={<FaShapes style={{ display: 'block' }} />}
>
<DialogButton onClick={() => installFromURL(pluginURL)}>Install</DialogButton>
<DialogButton disabled={pluginURL.length == 0} onClick={() => installFromURL(pluginURL)}>
Install
</DialogButton>
</Field>
</div>
);
@@ -1,7 +1,7 @@
import { DialogButton, staticClasses } from 'decky-frontend-lib';
import { FaTrash } from 'react-icons/fa';
import { DialogButton, Menu, MenuItem, showContextMenu, staticClasses } from 'decky-frontend-lib';
import { FaEllipsisH } from 'react-icons/fa';
import { useDeckyState } from '../../DeckyState';
import { useDeckyState } from '../../../DeckyState';
export default function PluginList() {
const { plugins } = useDeckyState();
@@ -22,9 +22,17 @@ export default function PluginList() {
<div className={staticClasses.Title} style={{ marginLeft: 'auto', boxShadow: 'none' }}>
<DialogButton
style={{ height: '40px', width: '40px', padding: '10px 12px' }}
onClick={() => window.DeckyPluginLoader.uninstall_plugin(name)}
onClick={(e: MouseEvent) =>
showContextMenu(
<Menu label="Plugin Actions">
<MenuItem onSelected={() => window.DeckyPluginLoader.importPlugin(name)}>Reload</MenuItem>
<MenuItem onSelected={() => window.DeckyPluginLoader.uninstallPlugin(name)}>Uninstall</MenuItem>
</Menu>,
e.currentTarget ?? window,
)
}
>
<FaTrash />
<FaEllipsisH />
</DialogButton>
</div>
</li>
+20 -11
View File
@@ -6,6 +6,7 @@ import {
Router,
SingleDropdownOption,
SuspensefulImage,
joinClassNames,
staticClasses,
} from 'decky-frontend-lib';
import { FC, useRef, useState } from 'react';
@@ -22,10 +23,6 @@ interface PluginCardProps {
plugin: StorePlugin | LegacyStorePlugin;
}
const classNames = (...classes: string[]) => {
return classes.join(' ');
};
function isLegacyPlugin(plugin: LegacyStorePlugin | StorePlugin): plugin is LegacyStorePlugin {
return 'artifact' in plugin;
}
@@ -44,7 +41,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
>
{/* TODO: abstract this messy focus hackiness into a custom component in lib */}
<Focusable
// className="Panel Focusable"
className="deckyStoreCard"
ref={containerRef}
onActivate={(_: CustomEvent) => {
buttonRef.current!.focus();
@@ -68,15 +65,17 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
boxSizing: 'border-box',
}}
>
<div style={{ display: 'flex', alignItems: 'center' }}>
<div className="deckyStoreCardHeader" style={{ display: 'flex', alignItems: 'center' }}>
<a
style={{ fontSize: '18pt', padding: '10px' }}
className={classNames(staticClasses.Text)}
className={joinClassNames(staticClasses.Text)}
// onClick={() => Router.NavigateToExternalWeb('https://github.com/' + plugin.artifact)}
>
{isLegacyPlugin(plugin) ? (
<div>
<span style={{ color: 'grey' }}>{plugin.artifact.split('/')[0]}/</span>
<div className="deckyStoreCardNameContainer">
<span className="deckyStoreCardLegacyRepoOwner" style={{ color: 'grey' }}>
{plugin.artifact.split('/')[0]}/
</span>
{plugin.artifact.split('/')[1]}
</div>
) : (
@@ -89,8 +88,10 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
display: 'flex',
flexDirection: 'row',
}}
className="deckyStoreCardBody"
>
<SuspensefulImage
className="deckyStoreCardImage"
suspenseWidth="256px"
style={{
width: 'auto',
@@ -113,14 +114,16 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
display: 'flex',
flexDirection: 'column',
}}
className="deckyStoreCardInfo"
>
<p className={classNames(staticClasses.PanelSectionRow)}>
<p className={joinClassNames(staticClasses.PanelSectionRow)}>
<span>Author: {plugin.author}</span>
</p>
<p className={classNames(staticClasses.PanelSectionRow)}>
<p className={joinClassNames('deckyStoreCardTagsContainer', staticClasses.PanelSectionRow)}>
<span>Tags:</span>
{plugin.tags.map((tag: string) => (
<span
className="deckyStoreCardTag"
style={{
padding: '5px',
marginRight: '10px',
@@ -133,6 +136,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
))}
{isLegacyPlugin(plugin) && (
<span
className="deckyStoreCardTag deckyStoreCardLegacyTag"
style={{
color: '#232120',
padding: '5px',
@@ -148,6 +152,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
</div>
</div>
<div
className="deckyStoreCardActionsContainer"
style={{
width: '100%',
alignSelf: 'flex-end',
@@ -156,6 +161,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
}}
>
<Focusable
className="deckyStoreCardActions"
style={{
display: 'flex',
flexDirection: 'row',
@@ -163,11 +169,13 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
}}
>
<div
className="deckyStoreCardInstallButtonContainer"
style={{
flex: '1',
}}
>
<DialogButton
className="deckyStoreCardInstallButton"
ref={buttonRef}
onClick={() =>
isLegacyPlugin(plugin)
@@ -179,6 +187,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
</DialogButton>
</div>
<div
className="deckyStoreCardVersionDropdownContainer"
style={{
flex: '0.2',
}}
+45 -13
View File
@@ -1,4 +1,4 @@
import { SteamSpinner } from 'decky-frontend-lib';
import { ModalRoot, SteamSpinner, showModal, staticClasses } from 'decky-frontend-lib';
import { FC, useEffect, useState } from 'react';
import PluginCard from './PluginCard';
@@ -35,19 +35,43 @@ export async function installFromURL(url: string) {
await fetch('http://localhost:1337/browser/install_plugin', {
method: 'POST',
body: formData,
credentials: 'include',
headers: {
Authentication: window.deckyAuthToken,
},
});
}
export async function requestLegacyPluginInstall(plugin: LegacyStorePlugin, selectedVer: string) {
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]);
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,
credentials: 'include',
headers: {
Authentication: window.deckyAuthToken,
},
});
}}
onCancel={() => {
// do nothing
}}
>
<div className={staticClasses.Title} style={{ flexDirection: 'column', boxShadow: 'unset' }}>
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) {
@@ -59,6 +83,10 @@ export async function requestPluginInstall(plugin: StorePlugin, selectedVer: Sto
await fetch('http://localhost:1337/browser/install_plugin', {
method: 'POST',
body: formData,
credentials: 'include',
headers: {
Authentication: window.deckyAuthToken,
},
});
}
@@ -68,12 +96,16 @@ const StorePage: FC<{}> = () => {
useEffect(() => {
(async () => {
const res = await fetch('https://beta.deckbrew.xyz/plugins', { method: 'GET' }).then((r) => r.json());
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());
const res = await fetch('https://plugins.deckbrew.xyz/get_plugins', {
method: 'GET',
}).then((r) => r.json());
console.log(res);
setLegacyData(res);
})();
+52 -13
View File
@@ -1,26 +1,65 @@
import { ButtonItem, CommonUIModule, webpackCache } from 'decky-frontend-lib';
import { forwardRef } from 'react';
import PluginLoader from './plugin-loader';
import { DeckyUpdater } from './updater';
declare global {
interface Window {
DeckyPluginLoader: PluginLoader;
DeckyUpdater?: DeckyUpdater;
importDeckyPlugin: Function;
syncDeckyPlugins: Function;
deckyHasLoaded: boolean;
deckyAuthToken: string;
webpackJsonp: any;
}
}
window.DeckyPluginLoader?.dismountAll();
window.DeckyPluginLoader?.deinit();
// HACK to fix plugins using webpack v4 push
window.DeckyPluginLoader = new PluginLoader();
window.importDeckyPlugin = function (name: string) {
window.DeckyPluginLoader?.importPlugin(name);
};
const v4Cache = {};
for (let m of Object.keys(webpackCache)) {
v4Cache[m] = { exports: webpackCache[m] };
}
window.syncDeckyPlugins = async function () {
const plugins = await (await fetch('http://127.0.0.1:1337/plugins')).json();
for (const plugin of plugins) {
window.DeckyPluginLoader?.importPlugin(plugin);
}
};
if (!window.webpackJsonp || window.webpackJsonp.deckyShimmed) {
window.webpackJsonp = {
deckyShimmed: true,
push: (mod: any): any => {
if (mod[1].get_require) return { c: v4Cache };
},
};
CommonUIModule.__deckyButtonItemShim = forwardRef((props: any, ref: any) => {
// tricks the old filter into working
const dummy = `childrenContainerWidth:"min"`;
return <ButtonItem ref={ref} _shim={dummy} {...props} />;
});
}
setTimeout(() => window.syncDeckyPlugins(), 5000);
(async () => {
window.deckyHasLoaded = true;
window.deckyAuthToken = await fetch('http://127.0.0.1:1337/auth/token').then((r) => r.text());
window.DeckyPluginLoader?.dismountAll();
window.DeckyPluginLoader?.deinit();
window.DeckyPluginLoader = new PluginLoader();
window.importDeckyPlugin = function (name: string) {
window.DeckyPluginLoader?.importPlugin(name);
};
window.syncDeckyPlugins = async function () {
const plugins = await (
await fetch('http://127.0.0.1:1337/plugins', {
credentials: 'include',
headers: { Authentication: window.deckyAuthToken },
})
).json();
for (const plugin of plugins) {
if (!window.DeckyPluginLoader.hasPlugin(plugin)) window.DeckyPluginLoader?.importPlugin(plugin);
}
};
setTimeout(() => window.syncDeckyPlugins(), 5000);
})();
+11 -1
View File
@@ -8,6 +8,16 @@ export const log = (name: string, ...args: any[]) => {
);
};
export const debug = (name: string, ...args: any[]) => {
console.debug(
`%c Decky %c ${name} %c`,
'background: #16a085; color: black;',
'background: #1abc9c; color: black;',
'color: blue;',
...args,
);
};
export const error = (name: string, ...args: any[]) => {
console.log(
`%c Decky %c ${name} %c`,
@@ -28,7 +38,7 @@ class Logger {
}
debug(...args: any[]) {
log(this.name, ...args);
debug(this.name, ...args);
}
}
+42 -20
View File
@@ -1,8 +1,9 @@
import { ModalRoot, QuickAccessTab, Router, showModal, sleep, staticClasses } from 'decky-frontend-lib';
import { ModalRoot, QuickAccessTab, showModal, staticClasses } from 'decky-frontend-lib';
import { FaPlug } from 'react-icons/fa';
import { DeckyState, DeckyStateContextProvider } from './components/DeckyState';
import LegacyPlugin from './components/LegacyPlugin';
import PluginInstallModal from './components/modals/PluginInstallModal';
import PluginView from './components/PluginView';
import SettingsPage from './components/settings';
import StorePage from './components/store/Store';
@@ -11,6 +12,8 @@ import Logger from './logger';
import { Plugin } from './plugin';
import RouterHook from './router-hook';
import TabsHook from './tabs-hook';
import Toaster from './toaster';
import { VerInfo, callUpdaterMethod } from './updater';
declare global {
interface Window {}
@@ -21,6 +24,7 @@ class PluginLoader extends Logger {
private tabsHook: TabsHook = new TabsHook();
// private windowHook: WindowHook = new WindowHook();
private routerHook: RouterHook = new RouterHook();
private toaster: Toaster = new Toaster();
private deckyState: DeckyState = new DeckyState();
private reloadLock: boolean = false;
@@ -53,29 +57,29 @@ class PluginLoader extends Logger {
});
}
public async notifyUpdates() {
const versionInfo = (await callUpdaterMethod('get_version')).result as VerInfo;
if (versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current) {
this.toaster.toast({
title: 'Decky',
body: `Update to ${versionInfo?.remote?.tag_name} availiable!`,
});
}
}
public addPluginInstallPrompt(artifact: string, version: string, request_id: string, hash: string) {
showModal(
<ModalRoot
onOK={async () => {
await this.callServerMethod('confirm_plugin_install', { request_id });
Router.NavigateBackOrOpenMenu();
await sleep(250);
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 1000);
}}
onCancel={() => {
this.callServerMethod('cancel_plugin_install', { request_id });
}}
>
<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>,
<PluginInstallModal
artifact={artifact}
version={version}
hash={hash}
onOK={() => this.callServerMethod('confirm_plugin_install', { request_id })}
onCancel={() => this.callServerMethod('cancel_plugin_install', { request_id })}
/>,
);
}
public uninstall_plugin(name: string) {
public uninstallPlugin(name: string) {
showModal(
<ModalRoot
onOK={async () => {
@@ -84,6 +88,10 @@ class PluginLoader extends Logger {
await fetch('http://localhost:1337/browser/uninstall_plugin', {
method: 'POST',
body: formData,
credentials: 'include',
headers: {
Authentication: window.deckyAuthToken,
},
});
}}
onCancel={() => {
@@ -97,6 +105,10 @@ class PluginLoader extends Logger {
);
}
public hasPlugin(name: string) {
return Boolean(this.plugins.find((plugin) => plugin.name == name));
}
public dismountAll() {
for (const plugin of this.plugins) {
this.log(`Dismounting ${plugin.name}`);
@@ -149,7 +161,12 @@ class PluginLoader extends Logger {
}
private async importReactPlugin(name: string) {
let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`);
let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, {
credentials: 'include',
headers: {
Authentication: window.deckyAuthToken,
},
});
if (res.ok) {
let plugin = await eval(await res.text())(this.createPluginAPI(name));
this.plugins.push({
@@ -171,8 +188,10 @@ class PluginLoader extends Logger {
async callServerMethod(methodName: string, args = {}) {
const response = await fetch(`http://127.0.0.1:1337/methods/${methodName}`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Authentication: window.deckyAuthToken,
},
body: JSON.stringify(args),
});
@@ -183,12 +202,15 @@ class PluginLoader extends Logger {
createPluginAPI(pluginName: string) {
return {
routerHook: this.routerHook,
toaster: this.toaster,
callServerMethod: this.callServerMethod,
async callPluginMethod(methodName: string, args = {}) {
const response = await fetch(`http://127.0.0.1:1337/plugins/${pluginName}/methods/${methodName}`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Authentication: window.deckyAuthToken,
},
body: JSON.stringify({
args,
+44 -8
View File
@@ -1,10 +1,11 @@
import { afterPatch, findModuleChild, unpatch } from 'decky-frontend-lib';
import { ReactElement, createElement, memo } from 'react';
import { ReactElement, ReactNode, cloneElement, createElement, memo } from 'react';
import type { Route } from 'react-router';
import {
DeckyRouterState,
DeckyRouterStateContextProvider,
RoutePatch,
RouterEntry,
useDeckyRouterState,
} from './components/DeckyRouterState';
@@ -38,14 +39,16 @@ class RouterHook extends Logger {
});
let Route: new () => Route;
// Used to store the new replicated routes we create to allow routes to be unpatched.
let toReplace = new Map<string, ReactNode>();
const DeckyWrapper = ({ children }: { children: ReactElement }) => {
const { routes } = useDeckyRouterState();
const { routes, routePatches } = useDeckyRouterState();
const routerIndex = children.props.children[0].props.children.length - 1;
if (
!children.props.children[0].props.children[routerIndex].length ||
children.props.children[0].props.children !== routes.size
) {
const routeList = children.props.children[0].props.children;
let routerIndex = routeList.length;
if (!routeList[routerIndex - 1]?.length || routeList[routerIndex - 1]?.length !== routes.size) {
if (routeList[routerIndex - 1]?.length && routeList[routerIndex - 1].length !== routes.size) routerIndex--;
const newRouterArray: ReactElement[] = [];
routes.forEach(({ component, props }, path) => {
newRouterArray.push(
@@ -54,8 +57,33 @@ class RouterHook extends Logger {
</Route>,
);
});
children.props.children[0].props.children[routerIndex] = newRouterArray;
routeList[routerIndex] = newRouterArray;
}
routeList.forEach((route: Route, index: number) => {
const replaced = toReplace.get(route?.props?.path as string);
if (replaced) {
routeList[index].props.children = replaced;
toReplace.delete(route?.props?.path as string);
}
if (route?.props?.path && routePatches.has(route.props.path as string)) {
toReplace.set(
route?.props?.path as string,
// @ts-ignore
routeList[index].props.children,
);
routePatches.get(route.props.path as string)?.forEach((patch) => {
const oType = routeList[index].props.children.type;
routeList[index].props.children = patch({
...routeList[index].props,
children: {
...cloneElement(routeList[index].props.children),
type: (props) => createElement(oType, props),
},
}).children;
});
}
});
this.debug('Rerendered routes list');
return children;
};
@@ -92,6 +120,14 @@ class RouterHook extends Logger {
this.routerState.addRoute(path, component, props);
}
addPatch(path: string, patch: RoutePatch) {
return this.routerState.addPatch(path, patch);
}
removePatch(path: string, patch: RoutePatch) {
this.routerState.removePatch(path, patch);
}
removeRoute(path: string) {
this.routerState.removeRoute(path);
}
+3 -3
View File
@@ -103,19 +103,19 @@ class TabsHook extends Logger {
}
deinit() {
unpatch(this.cNode.stateNode, 'render');
if (this.cNode) 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) {
this.log('Adding tab', tab.id, 'to render array');
this.debug('Adding tab', tab.id, 'to render array');
this.tabs.push(tab);
}
removeById(id: number) {
this.log('Removing tab', id);
this.debug('Removing tab', id);
this.tabs = this.tabs.filter((tab) => tab.id !== id);
}
+93
View File
@@ -0,0 +1,93 @@
import { ToastData, afterPatch, findInReactTree, findModuleChild, sleep, unpatch } from 'decky-frontend-lib';
import Toast from './components/Toast';
import Logger from './logger';
declare global {
interface Window {
__TOASTER_INSTANCE: any;
NotificationStore: any;
}
}
class Toaster extends Logger {
private instanceRet: any;
private node: any;
private settingsModule: any;
constructor() {
super('Toaster');
window.__TOASTER_INSTANCE?.deinit?.();
window.__TOASTER_INSTANCE = this;
this.init();
}
async init() {
this.settingsModule = findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (typeof m[prop]?.settings?.bDisableToastsInGame !== 'undefined') return m[prop];
}
});
let instance: any;
while (true) {
instance = findInReactTree(
(document.getElementById('root') as any)._reactRootContainer._internalRoot.current,
(x) => x?.memoizedProps?.className?.startsWith?.('toastmanager_ToastPlaceholder'),
);
if (instance) break;
this.debug('finding instance');
await sleep(2000);
}
this.node = instance.return.return;
this.node.stateNode.render = (...args: any[]) => {
const ret = this.node.stateNode.__proto__.render.call(this.node.stateNode, ...args);
if (ret) {
this.instanceRet = ret;
afterPatch(ret, 'type', (_: any, ret: any) => {
if (ret?.props?.children[1]?.children?.props?.notification?.decky) {
const toast = ret.props.children[1].children.props.notification;
ret.props.children[1].children.type = () => <Toast toast={toast} />;
}
return ret;
});
}
return ret;
};
this.node.stateNode.forceUpdate();
this.log('Initialized');
}
toast(toast: ToastData) {
const settings = this.settingsModule.settings;
let toastData = {
nNotificationID: window.NotificationStore.m_nNextTestNotificationID++,
rtCreated: Date.now(),
eType: 15,
nToastDurationMS: toast.duration || 5e3,
data: toast,
decky: true,
};
// @ts-ignore
toastData.data.appid = () => 0;
if (
(settings.bDisableAllToasts && !toast.critical) ||
(settings.bDisableToastsInGame && !toast.critical && window.NotificationStore.BIsUserInGame())
)
return;
window.NotificationStore.m_rgNotificationToasts.push(toastData);
window.NotificationStore.DispatchNextToast();
window.NotificationStore.m_rgNotificationToasts.pop();
}
deinit() {
this.instanceRet && unpatch(this.instanceRet, 'type');
this.node && delete this.node.stateNode.render;
this.node && this.node.stateNode.forceUpdate();
}
}
export default Toaster;
+48
View File
@@ -0,0 +1,48 @@
import { sleep } from 'decky-frontend-lib';
export enum Branches {
Release,
Prerelease,
Nightly,
}
export interface DeckyUpdater {
updateProgress: (val: number) => void;
finish: () => void;
}
export 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 async function callUpdaterMethod(methodName: string, args = {}) {
const response = await fetch(`http://127.0.0.1:1337/updater/${methodName}`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Authentication: window.deckyAuthToken,
},
body: JSON.stringify(args),
});
return response.json();
}
export async function finishUpdate() {
callUpdaterMethod('do_restart');
await sleep(3000);
location.reload();
}
+1
View File
@@ -5,6 +5,7 @@
"target": "ES2020",
"jsx": "react",
"jsxFactory": "window.SP_REACT.createElement",
"jsxFragmentFactory": "window.SP_REACT.Fragment",
"declaration": false,
"moduleResolution": "node",
"noUnusedLocals": true,