Compare commits

...

51 Commits

Author SHA1 Message Date
TrainDoctor 16a6e9b6a9 Give SettingsManager a default directory 2022-08-31 12:20:31 -07:00
Philipp Richter 6f84cf94b5 Fix LimitOverrunError when a lot of data is passed to the backend (fixes #158) (#161)
* Fix LimitOverrunError when a lot of data is passed to the backend

* Raise socket's buffer limit to something more sensible
2022-08-31 10:46:40 -07:00
TrainDoctor 7c06db5ece Moved traceback to higher log level for cleaner logs 2022-08-29 15:18:18 -07:00
TrainDoctor aeb2decfc1 Fix plugin uninstalling 2022-08-29 14:41:10 -07:00
botato b7d7ca04e1 Refractor plugin backend (#111)
* refractor uninstall plugin backend

* refractor plugin installation method

* Change formatting in browser.py

* Manually format main.py

* Manually format utilities.py

* remove inconsistency

* remove unnecessary linebreaks

* lol what

* last minute pythoning

* Fix async missing

* lint

* more refractor

* await forgotten

* fix: menu not disappearing after first click

* lint

* bug: fix double click on uninstall

* depricate request installs

* basic patch notes viewer, lazy-load settings and store, build frontend as esmodule, add lazy-loaded react-markdown, backend changes to accomodate ESModule frontend

* refractor uninstall plugin backend

* Change formatting in browser.py

* Manually format main.py

* Manually format utilities.py

* remove unnecessary linebreaks

* lol what

* last minute pythoning

* Fix async missing

* rebase onto main

* fix error, fix React crash if patch notes are opened before remote version info is loaded

Co-authored-by: TrainDoctor <traindoctor@protonmail.com>
Co-authored-by: AAGaming <aa@mail.catvibers.me>
2022-08-26 21:01:23 -07:00
AAGaming d4d1c2bbab basic patch notes viewer, lazy-load settings and store, build frontend as esmodule, add lazy-loaded react-markdown, backend changes to accomodate ESModule frontend 2022-08-26 01:18:28 -04:00
TrainDoctor effc4ab0f5 Hide branch select until it's working 2022-08-24 21:18:38 -07:00
AAGaming 79db0c779d Settings API for loader, preview branch select 2022-08-24 23:51:20 -04:00
AAGaming fe2b6b0283 fix plugins menu scrolling, shut up typecript 2022-08-24 20:49:39 -04:00
Chris Simons b9a87cd785 set gid along with uid (#152) 2022-08-24 17:22:07 -07:00
AAGaming 98e9ce881f move DeckyUpdater to effect 2022-08-24 20:03:29 -04:00
TrainDoctor e49bdd9c05 Move conditional check to correct location 2022-08-24 16:56:20 -07:00
TrainDoctor d0fd2ac674 Move to using deckyState 2022-08-24 13:47:21 -07:00
TrainDoctor de1c89af21 Fix updater being corrupted by multiple attempts to download 2022-08-23 15:23:23 -07:00
AAGaming 8b3f569a09 Add plugin updater, notification badge, fixes 2022-08-21 16:41:25 -04:00
Collin Diekvoss 1930400032 Better wrapping of plugin tags (#150) 2022-08-20 21:40:57 -04:00
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
44 changed files with 2081 additions and 432 deletions
+63 -13
View File
@@ -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
@@ -105,6 +99,12 @@ jobs:
name: PluginLoader
path: dist
- name: Install semver-tool asdf
uses: asdf-vm/actions/install@v1
with:
tool_versions: |
semver 3.3.0
- name: Get tag 🏷️
id: old_tag
uses: rafarlopes/get-latest-pre-release-tag-action@v1
@@ -112,20 +112,22 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
repository: 'decky-loader'
- name: Prepare tag ⚙️
id: ready_tag
run: |
export VERSION=${{ steps.old_tag.outputs.tag }}
export COMMIT=$(git log -1 --pretty=format:%h)
echo ::set-output name=tag_name::$(sed -r 's/(-.*)?-pre$//' <<< $VERSION)-$COMMIT-pre
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: Nightly ${{ steps.ready_tag.outputs.tag_name }}
message: Pre-release ${{ steps.ready_tag.outputs.tag_name }}
- name: Release 📦
uses: softprops/action-gh-release@v1
@@ -137,6 +139,54 @@ jobs:
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' }}
+47 -59
View File
@@ -1,20 +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 injector import inject_to_tab
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:
@@ -24,25 +28,22 @@ class PluginInstallContext:
self.hash = hash
class PluginBrowser:
def __init__(self, plugin_path, server_instance, plugins) -> None:
self.log = getLogger("browser")
def __init__(self, plugin_path, plugins) -> None:
self.plugin_path = plugin_path
self.plugins = plugins
self.install_requests = {}
server_instance.add_routes([
web.post("/browser/install_plugin", self.install_plugin),
web.post("/browser/uninstall_plugin", self.uninstall_plugin)
])
def _unzip_to_plugin_dir(self, zip, name, hash):
zip_hash = sha256(zip.getbuffer()).hexdigest()
if hash and (zip_hash != hash):
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):
@@ -54,62 +55,49 @@ class PluginBrowser:
if plugin['name'] == name:
return path.join(self.plugin_path, folder)
except:
self.log.debug(f"skipping {folder}")
logger.debug(f"skipping {folder}")
async def uninstall_plugin(self, name):
tab = await get_tab("SP")
try:
if type(name) != str:
data = await name.post()
name = data.get("name", "undefined")
self.log.info("uninstalling " + name)
self.log.info(" at dir " + self.find_plugin_folder(name))
logger.info("uninstalling " + name)
logger.info(" at dir " + self.find_plugin_folder(name))
logger.debug("unloading %s" % str(name))
await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
if self.plugins[name]:
self.plugins[name].stop()
self.plugins.pop(name, None)
del self.plugins[name]
logger.debug("removing files %s" % str(name))
rmtree(self.find_plugin_folder(name))
except FileNotFoundError:
self.log.warning(f"Plugin {name} not installed, skipping uninstallation")
return web.Response(text="Requested plugin uninstall")
logger.warning(f"Plugin {name} not installed, skipping uninstallation")
except Exception as e:
logger.error(f"Plugin {name} in {self.find_plugin_folder(name)} was not uninstalled")
logger.error(f"Error at %s", exc_info=e)
async def _install(self, artifact, name, version, hash):
try:
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
)
if ret:
self.log.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.debug("Unzipping...")
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
if ret:
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})")
else:
self.log.fatal(f"Could not fetch from URL. {await res.text()}")
async def install_plugin(self, request):
data = await request.post()
get_event_loop().create_task(self.request_plugin_install(data.get("artifact", ""), data.get("name", "No name"), data.get("version", "dev"), data.get("hash", False)))
return web.Response(text="Requested plugin install")
logger.fatal(f"Could not fetch from URL. {await res.text()}")
async def request_plugin_install(self, artifact, name, version, hash):
request_id = str(time())
@@ -117,7 +105,7 @@ class PluginBrowser:
tab = await get_tab("SP")
await tab.open_websocket()
await tab.evaluate_js(f"DeckyPluginLoader.addPluginInstallPrompt('{name}', '{version}', '{request_id}', '{hash}')")
async def confirm_plugin_install(self, request_id):
request = self.install_requests.pop(request_id)
await self._install(request.artifact, request.name, request.version, request.hash)
+93 -2
View File
@@ -1,7 +1,98 @@
import re
import ssl
import certifi
import subprocess
import uuid
from subprocess import check_output
from time import sleep
import certifi
from aiohttp.web import Response, middleware
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/.*")
frontend_regex = re.compile("^/frontend/.*")
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)) or frontend_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
# Get the default home path unless a user is specified
def get_home_path(username = None) -> str:
if username == None:
raise ValueError("Username not defined, no home path can be found.")
else:
return str("/home/"+username)
# Get the default homebrew path unless a user is specified
def get_homebrew_path(home_path = None) -> str:
if home_path == None:
raise ValueError("Home path not defined, homebrew dir cannot be determined.")
else:
return str(home_path+"/homebrew")
# return str(home_path+"/homebrew")
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.PIPE, stderr=subprocess.STDOUT)
async def start_systemd_unit(unit_name: str) -> subprocess.CompletedProcess:
cmd = ["systemctl", "start", unit_name]
return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+6 -3
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"
@@ -65,11 +66,13 @@ 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())
logger.error(format_exc())
await sleep(5)
else:
break
if res.status == 200:
r = await res.json()
+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,
+14 -8
View File
@@ -1,4 +1,4 @@
from asyncio import Queue
from asyncio import Queue, sleep
from json.decoder import JSONDecodeError
from logging import getLogger
from os import listdir, path
@@ -75,10 +75,11 @@ class Loader:
self.loop.create_task(self.handle_reloads())
server_instance.add_routes([
web.get("/frontend/{path:.*}", self.handle_frontend_assets),
web.get("/plugins", self.get_plugins),
web.get("/plugins/{plugin_name}/frontend_bundle", self.handle_frontend_bundle),
web.post("/plugins/{plugin_name}/methods/{method_name}", self.handle_plugin_method_call),
web.get("/plugins/{plugin_name}/assets/{path:.*}", self.handle_frontend_assets),
web.get("/plugins/{plugin_name}/assets/{path:.*}", self.handle_plugin_frontend_assets),
# The following is legacy plugin code.
web.get("/plugins/load_main/{name}", self.load_plugin_main_view),
@@ -86,15 +87,20 @@ class Loader:
web.get("/steam_resource/{path:.+}", self.get_steam_resource)
])
async def handle_frontend_assets(self, request):
file = path.join(path.dirname(__file__), "static", request.match_info["path"])
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
async def get_plugins(self, request):
plugins = list(self.plugins.values())
return web.json_response([str(i) if not i.legacy else "$LEGACY_"+str(i) for i in plugins])
return web.json_response([{"name": str(i) if not i.legacy else "$LEGACY_"+str(i), "version": i.version} for i in plugins])
def handle_frontend_assets(self, request):
def handle_plugin_frontend_assets(self, request):
plugin = self.plugins[request.match_info["plugin_name"]]
file = path.join(self.plugin_path, plugin.plugin_directory, "dist/assets", request.match_info["path"])
return web.FileResponse(file)
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
def handle_frontend_bundle(self, request):
plugin = self.plugins[request.match_info["plugin_name"]]
@@ -116,13 +122,13 @@ class Loader:
self.logger.info(f"Plugin {plugin.name} is passive")
self.plugins[plugin.name] = plugin.start()
self.logger.info(f"Loaded {plugin.name}")
self.loop.create_task(self.dispatch_plugin(plugin.name if not plugin.legacy else "$LEGACY_" + plugin.name))
self.loop.create_task(self.dispatch_plugin(plugin.name if not plugin.legacy else "$LEGACY_" + plugin.name, plugin.version))
except Exception as e:
self.logger.error(f"Could not load {file}. {e}")
print_exc()
async def dispatch_plugin(self, name):
await inject_to_tab("SP", f"window.importDeckyPlugin('{name}')")
async def dispatch_plugin(self, name, version):
await inject_to_tab("SP", f"window.importDeckyPlugin('{name}', '{version}')")
def import_plugins(self):
self.logger.info(f"import plugins from {self.plugin_path}")
+60 -27
View File
@@ -1,38 +1,58 @@
# Full imports
from asyncio import get_event_loop, sleep
from json import dumps, loads
from logging import DEBUG, INFO, basicConfig, getLogger
from os import getenv
from os import getenv, path
from subprocess import call
import aiohttp_cors
# Partial imports
from aiohttp import ClientSession
from aiohttp.web import Application, Response, get, run_app, static
from aiohttp_jinja2 import setup as jinja_setup
# local modules
from browser import PluginBrowser
from helpers import (REMOTE_DEBUGGER_UNIT, csrf_middleware, get_csrf_token,
get_home_path, get_homebrew_path, get_user,
get_user_group, set_user, set_user_group,
stop_systemd_unit)
from injector import inject_to_tab, tab_has_global_var
from loader import Loader
from settings import SettingsManager
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")),
"live_reload": getenv("LIVE_RELOAD", "1") == "1",
"log_level": {"CRITICAL": 50, "ERROR": 40, "WARNING":30, "INFO": 20, "DEBUG": 10}[getenv("LOG_LEVEL", "INFO")]
"log_level": {"CRITICAL": 50, "ERROR": 40, "WARNING": 30, "INFO": 20, "DEBUG": 10}[
getenv("LOG_LEVEL", "INFO")
],
}
basicConfig(level=CONFIG["log_level"], format="[%(module)s][%(levelname)s]: %(message)s")
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
from updater import Updater
basicConfig(
level=CONFIG["log_level"],
format="[%(module)s][%(levelname)s]: %(message)s"
)
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})")
@@ -41,24 +61,32 @@ 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="*")
"https://steamloopback.host": aiohttp_cors.ResourceOptions(
expose_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_loader.plugins)
self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.plugin_loader.plugins)
self.settings = SettingsManager("loader", path.join(HOMEBREW_PATH, "settings"))
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 self.settings.getSetting("cef_forward", False):
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.cors.add(route)
self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), 'static'))])
self.web_app.add_routes([static("/legacy", path.join(path.dirname(__file__), 'legacy'))])
@@ -67,6 +95,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:
@@ -79,9 +110,11 @@ class PluginManager:
async def load_plugins(self):
await self.wait_for_server()
self.plugin_loader.import_plugins()
#await inject_to_tab("SP", "window.syncDeckyPlugins();")
# await inject_to_tab("SP", "window.syncDeckyPlugins();")
async def loader_reinjector(self):
await sleep(2)
await self.inject_javascript()
while True:
await sleep(5)
if not await tab_has_global_var("SP", "deckyHasLoaded"):
@@ -90,7 +123,7 @@ class PluginManager:
async def inject_javascript(self, request=None):
try:
await inject_to_tab("SP", "try{" + open(path.join(path.dirname(__file__), "./static/plugin-loader.iife.js"), "r").read() + "}catch(e){console.error(e)}", True)
await inject_to_tab("SP", "try{window.deckyHasLoaded = true;(async()=>{while(!window.SP_REACT){await new Promise(r => setTimeout(r, 10))};await import('http://localhost:1337/frontend/index.js')})();}catch(e){console.error(e)}", True)
except:
logger.info("Failed to inject JavaScript into tab")
pass
+39 -6
View File
@@ -1,17 +1,19 @@
import multiprocessing
from asyncio import (Lock, get_event_loop, new_event_loop,
open_unix_connection, set_event_loop, sleep,
start_unix_server)
start_unix_server, IncompleteReadError, LimitOverrunError)
from concurrent.futures import ProcessPoolExecutor
from importlib.util import module_from_spec, spec_from_file_location
from json import dumps, load, loads
from os import path, setuid
from os import path, setgid, setuid
from signal import SIGINT, signal
from sys import exit
from time import time
multiprocessing.set_start_method("fork")
BUFFER_LIMIT = 2 ** 20 # 1 MiB
class PluginWrapper:
def __init__(self, file, plugin_directory, plugin_path) -> None:
self.file = file
@@ -21,7 +23,13 @@ class PluginWrapper:
self.socket_addr = f"/tmp/plugin_socket_{time()}"
self.method_call_lock = Lock()
self.version = None
json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r"))
if path.isfile(path.join(plugin_path, plugin_directory, "package.json")):
package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r"))
self.version = package_json["version"]
self.legacy = False
self.main_view_html = json["main_view_html"] if "main_view_html" in json else ""
@@ -43,6 +51,7 @@ class PluginWrapper:
set_event_loop(new_event_loop())
if self.passive:
return
setgid(0 if "root" in self.flags else 1000)
setuid(0 if "root" in self.flags else 1000)
spec = spec_from_file_location("_", self.file)
module = module_from_spec(spec)
@@ -55,11 +64,23 @@ class PluginWrapper:
get_event_loop().run_forever()
async def _setup_socket(self):
self.socket = await start_unix_server(self._listen_for_method_call, path=self.socket_addr)
self.socket = await start_unix_server(self._listen_for_method_call, path=self.socket_addr, limit=BUFFER_LIMIT)
async def _listen_for_method_call(self, reader, writer):
while True:
data = loads((await reader.readline()).decode("utf-8"))
line = bytearray()
while True:
try:
line.extend(await reader.readuntil())
except LimitOverrunError:
line.extend(await reader.read(reader._limit))
continue
except IncompleteReadError as err:
line.extend(err.partial)
break
else:
break
data = loads(line.decode("utf-8"))
if "stop" in data:
get_event_loop().stop()
while get_event_loop().is_running():
@@ -81,7 +102,7 @@ class PluginWrapper:
retries = 0
while retries < 10:
try:
self.reader, self.writer = await open_unix_connection(self.socket_addr)
self.reader, self.writer = await open_unix_connection(self.socket_addr, limit=BUFFER_LIMIT)
return True
except:
await sleep(2)
@@ -114,7 +135,19 @@ class PluginWrapper:
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"))
line = bytearray()
while True:
try:
line.extend(await self.reader.readuntil())
except LimitOverrunError:
line.extend(await self.reader.read(self.reader._limit))
continue
except IncompleteReadError as err:
line.extend(err.partial)
break
else:
break
res = loads(line.decode("utf-8"))
if not res["success"]:
raise Exception(res["res"])
return res["res"]
+44
View File
@@ -0,0 +1,44 @@
import imp
from json import dump, load
from os import mkdir, path
from helpers import get_home_path, get_homebrew_path, get_user, set_user
class SettingsManager:
def __init__(self, name, settings_directory = None) -> None:
set_user()
USER = get_user()
if settings_directory == None:
settings_directory = get_homebrew_path(get_home_path(USER))
self.path = path.join(settings_directory, name + ".json")
if not path.exists(settings_directory):
mkdir(settings_directory)
self.settings = {}
try:
open(self.path, "x")
except FileExistsError as e:
self.read()
pass
def read(self):
try:
with open(self.path, "r") as file:
self.settings = load(file)
except Exception as e:
print(e)
pass
def commit(self):
with open(self.path, "w+") as file:
dump(self.settings, file, indent=4)
def getSetting(self, key, default):
return self.settings.get(key, default)
def setSetting(self, key, value):
self.settings[key] = value
self.commit()
+8 -3
View File
@@ -26,6 +26,7 @@ class Updater:
"check_for_updates": self.check_for_updates
}
self.remoteVer = None
self.allRemoteVers = None
try:
with open(path.join(getcwd(), ".loader.version"), 'r') as version_file:
self.localVer = version_file.readline().replace("\n", "")
@@ -59,17 +60,21 @@ class Updater:
return {
"current": self.localVer,
"remote": self.remoteVer,
"all": self.allRemoteVers,
"updatable": self.localVer != None
}
else:
return {"current": "unknown", "updatable": False}
return {"current": "unknown", "remote": self.remoteVer, "all": self.allRemoteVers, "updatable": False}
async def check_for_updates(self):
async with ClientSession() as web:
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", ssl=helpers.get_ssl_context()) as res:
remoteVersions = await res.json()
self.remoteVer = next(filter(lambda ver: ver["prerelease"] and ver["tag_name"].startswith("v") and ver["tag_name"].endswith("-pre"), remoteVersions), None)
self.allRemoteVers = remoteVersions
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):
@@ -79,7 +84,7 @@ class Updater:
await self.check_for_updates()
except:
pass
await sleep(60 * 60) # 1 hour
await sleep(60 * 60 * 6) # 6 hours
async def do_update(self):
version = self.remoteVer["tag_name"]
+38 -5
View File
@@ -5,6 +5,8 @@ from aiohttp import ClientSession, web
from injector import inject_to_tab
import helpers
import subprocess
class Utilities:
def __init__(self, context) -> None:
@@ -12,11 +14,17 @@ class Utilities:
self.util_methods = {
"ping": self.ping,
"http_request": self.http_request,
"install_plugin": self.install_plugin,
"cancel_plugin_install": self.cancel_plugin_install,
"confirm_plugin_install": self.confirm_plugin_install,
"uninstall_plugin": self.uninstall_plugin,
"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,
"set_setting": self.set_setting,
"get_setting": self.get_setting
}
if context:
@@ -40,12 +48,23 @@ class Utilities:
res["success"] = False
return web.json_response(res)
async def install_plugin(self, artifact="", name="No name", version="dev", hash=False):
return await self.context.plugin_browser.request_plugin_install(
artifact=artifact,
name=name,
version=version,
hash=hash
)
async def confirm_plugin_install(self, request_id):
return await self.context.plugin_browser.confirm_plugin_install(request_id)
def cancel_plugin_install(self, request_id):
return self.context.plugin_browser.cancel_plugin_install(request_id)
async def uninstall_plugin(self, name):
return await self.context.plugin_browser.uninstall_plugin(name)
async def http_request(self, method="", url="", **kwargs):
async with ClientSession() as web:
async with web.request(method, url, ssl=helpers.get_ssl_context(), **kwargs) as res:
@@ -69,12 +88,12 @@ class Utilities:
return {
"success": True,
"result" : result["result"]["result"].get("value")
"result": result["result"]["result"].get("value")
}
except Exception as e:
return {
"success": False,
"result": e
"success": False,
"result": e
}
async def inject_css_into_tab(self, tab, style):
@@ -99,7 +118,7 @@ class Utilities:
return {
"success": True,
"result" : css_id
"result": css_id
}
except Exception as e:
return {
@@ -133,3 +152,17 @@ class Utilities:
"success": False,
"result": e
}
async def get_setting(self, key, default):
return self.context.settings.getSetting(key, default)
async def set_setting(self, key, value):
return self.context.settings.setSetting(key, value)
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
+4 -3
View File
@@ -4,12 +4,13 @@
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
RELEASE=$(curl -s 'https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases' | jq -r "first(.[] | select(.prerelease == "true"))")
+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"
+6 -2
View File
@@ -27,6 +27,8 @@
"react": "16.14.0",
"react-dom": "16.14.0",
"rollup": "^2.76.0",
"rollup-plugin-external-globals": "^0.6.1",
"rollup-plugin-polyfill-node": "^0.10.2",
"tslib": "^2.4.0",
"typescript": "^4.7.4"
},
@@ -37,7 +39,9 @@
}
},
"dependencies": {
"decky-frontend-lib": "^1.2.4",
"react-icons": "^4.4.0"
"decky-frontend-lib": "^1.8.0",
"react-icons": "^4.4.0",
"react-markdown": "^8.0.3",
"remark-gfm": "^3.0.1"
}
}
+714 -9
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.2.4
decky-frontend-lib: ^1.8.0
husky: ^8.0.1
import-sort-style-module: ^6.0.0
inquirer: ^8.2.4
@@ -18,13 +18,19 @@ specifiers:
react: 16.14.0
react-dom: 16.14.0
react-icons: ^4.4.0
react-markdown: ^8.0.3
remark-gfm: ^3.0.1
rollup: ^2.76.0
rollup-plugin-external-globals: ^0.6.1
rollup-plugin-polyfill-node: ^0.10.2
tslib: ^2.4.0
typescript: ^4.7.4
dependencies:
decky-frontend-lib: 1.2.4
decky-frontend-lib: 1.8.0
react-icons: 4.4.0_react@16.14.0
react-markdown: 8.0.3_vshvapmxg47tngu7tvrsqpq55u
remark-gfm: 3.0.1
devDependencies:
'@rollup/plugin-commonjs': 21.1.0_rollup@2.76.0
@@ -43,6 +49,8 @@ devDependencies:
react: 16.14.0
react-dom: 16.14.0_react@16.14.0
rollup: 2.76.0
rollup-plugin-external-globals: 0.6.1_rollup@2.76.0
rollup-plugin-polyfill-node: 0.10.2_rollup@2.76.0
tslib: 2.4.0
typescript: 4.7.4
@@ -304,6 +312,17 @@ packages:
rollup: 2.76.0
dev: true
/@rollup/plugin-inject/4.0.4_rollup@2.76.0:
resolution: {integrity: sha512-4pbcU4J/nS+zuHk+c+OL3WtmEQhqxlZ9uqfjQMQDOHOPld7PsCd8k5LWs8h5wjwJN7MgnAn768F2sDxEP4eNFQ==}
peerDependencies:
rollup: ^1.20.0 || ^2.0.0
dependencies:
'@rollup/pluginutils': 3.1.0_rollup@2.76.0
estree-walker: 2.0.2
magic-string: 0.25.9
rollup: 2.76.0
dev: true
/@rollup/plugin-json/4.1.0_rollup@2.76.0:
resolution: {integrity: sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==}
peerDependencies:
@@ -368,6 +387,20 @@ packages:
rollup: 2.76.0
dev: true
/@rollup/pluginutils/4.2.1:
resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
engines: {node: '>= 8.0.0'}
dependencies:
estree-walker: 2.0.2
picomatch: 2.3.1
dev: true
/@types/debug/4.1.7:
resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==}
dependencies:
'@types/ms': 0.7.31
dev: false
/@types/eslint-scope/3.7.4:
resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==}
dependencies:
@@ -394,6 +427,12 @@ packages:
resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==}
dev: true
/@types/hast/2.3.4:
resolution: {integrity: sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==}
dependencies:
'@types/unist': 2.0.6
dev: false
/@types/history/4.7.11:
resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==}
dev: true
@@ -402,13 +441,26 @@ packages:
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
dev: true
/@types/mdast/3.0.10:
resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==}
dependencies:
'@types/unist': 2.0.6
dev: false
/@types/mdurl/1.0.2:
resolution: {integrity: sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==}
dev: false
/@types/ms/0.7.31:
resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
dev: false
/@types/node/18.0.4:
resolution: {integrity: sha512-M0+G6V0Y4YV8cqzHssZpaNCqvYwlCiulmm0PwpNLF55r/+cT8Ol42CHRU1SEaYFH2rTwiiE1aYg/2g2rrtGdPA==}
dev: true
/@types/prop-types/15.7.5:
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
dev: true
/@types/react-router/5.1.18:
resolution: {integrity: sha512-YYknwy0D0iOwKQgz9v8nOzt2J6l4gouBmDnWqUUznltOTaon+r8US8ky8HvN0tXvc38U9m6z/t2RsVsnd1zM0g==}
@@ -422,7 +474,6 @@ packages:
dependencies:
'@types/prop-types': 15.7.5
csstype: 3.1.0
dev: true
/@types/resolve/1.17.1:
resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
@@ -430,6 +481,10 @@ packages:
'@types/node': 18.0.4
dev: true
/@types/unist/2.0.6:
resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
dev: false
/@types/webpack/5.28.0:
resolution: {integrity: sha512-8cP0CzcxUiFuA9xGJkfeVpqmWTk9nx6CWwamRGCj95ph1SmlRRk9KlCZ6avhCbZd4L68LvYT6l1kpdEnQXrF8w==}
dependencies:
@@ -620,6 +675,10 @@ packages:
sprintf-js: 1.0.3
dev: true
/bail/2.0.2:
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
dev: false
/balanced-match/1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
dev: true
@@ -693,6 +752,10 @@ packages:
resolution: {integrity: sha512-yy7XLWCubDobokgzudpkKux8e0UOOnLHE6mlNJBzT3lZJz6s5atSEzjoL+fsCPkI0G8MP5uVdDx1ur/fXEWkZA==}
dev: true
/ccount/2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
dev: false
/chalk/2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
engines: {node: '>=4'}
@@ -710,6 +773,10 @@ packages:
supports-color: 7.2.0
dev: true
/character-entities/2.0.2:
resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
dev: false
/chardet/0.7.0:
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
dev: true
@@ -762,6 +829,10 @@ packages:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
dev: true
/comma-separated-tokens/2.0.2:
resolution: {integrity: sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg==}
dev: false
/commander/2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
dev: true
@@ -792,7 +863,6 @@ packages:
/csstype/3.1.0:
resolution: {integrity: sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==}
dev: true
/debug/4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
@@ -804,10 +874,17 @@ packages:
optional: true
dependencies:
ms: 2.1.2
dev: true
/decky-frontend-lib/1.2.4:
resolution: {integrity: sha512-r3mLEey9KUkF68geJVSjNlOz/Fg4vpMKUzoutSceyd8o/J5l+QR+Vf0b3gwK3UN9Sp4Pj4XQ1eB82+/W0ApsFg==}
/decky-frontend-lib/1.8.0:
resolution: {integrity: sha512-ZPJdbkNI5d/d/19Hv97FVgFyMerYUzwXavYGv8euLMrbH9XuVM4KAx0mbKkO0SQUl75HKQ3LxsaCpA6QLDr9EA==}
dependencies:
minimist: 1.2.6
dev: false
/decode-named-character-reference/1.0.2:
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
dependencies:
character-entities: 2.0.2
dev: false
/deepmerge/4.2.2:
@@ -821,11 +898,21 @@ packages:
clone: 1.0.4
dev: true
/dequal/2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
dev: false
/detect-newline/2.1.0:
resolution: {integrity: sha512-CwffZFvlJffUg9zZA0uqrjQayUTC8ob94pnr5sFwaVv3IOmkfUHcWH+jXaQK3askE51Cqe8/9Ql/0uXNwqZ8Zg==}
engines: {node: '>=0.10.0'}
dev: true
/diff/5.1.0:
resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==}
engines: {node: '>=0.3.1'}
dev: false
/electron-to-chromium/1.4.189:
resolution: {integrity: sha512-dQ6Zn4ll2NofGtxPXaDfY2laIa6NyCQdqXYHdwH90GJQW0LpJJib0ZU/ERtbb0XkBEmUD2eJtagbOie3pdMiPg==}
dev: true
@@ -862,6 +949,11 @@ packages:
engines: {node: '>=0.8.0'}
dev: true
/escape-string-regexp/5.0.0:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'}
dev: false
/eslint-scope/5.1.1:
resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==}
engines: {node: '>=8.0.0'}
@@ -906,6 +998,10 @@ packages:
engines: {node: '>=0.8.x'}
dev: true
/extend/3.0.2:
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
dev: false
/external-editor/3.1.0:
resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==}
engines: {node: '>=4'}
@@ -1000,6 +1096,10 @@ packages:
function-bind: 1.1.1
dev: true
/hast-util-whitespace/2.0.0:
resolution: {integrity: sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg==}
dev: false
/husky/8.0.1:
resolution: {integrity: sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==}
engines: {node: '>=14'}
@@ -1085,6 +1185,10 @@ packages:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
dev: true
/inline-style-parser/0.1.1:
resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==}
dev: false
/inquirer/8.2.4:
resolution: {integrity: sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==}
engines: {node: '>=12.0.0'}
@@ -1110,6 +1214,11 @@ packages:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
dev: true
/is-buffer/2.0.5:
resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==}
engines: {node: '>=4'}
dev: false
/is-builtin-module/3.1.0:
resolution: {integrity: sha512-OV7JjAgOTfAFJmHZLvpSTb4qi0nIILDV1gWPYDnDJUTNFM5aGlRAhk4QcT8i7TuAleeEV5Fdkqn3t4mS+Q11fg==}
engines: {node: '>=6'}
@@ -1142,6 +1251,11 @@ packages:
resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==}
dev: true
/is-plain-obj/4.1.0:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
dev: false
/is-reference/1.2.1:
resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==}
dependencies:
@@ -1197,6 +1311,11 @@ packages:
hasBin: true
dev: true
/kleur/4.1.5:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
dev: false
/loader-runner/4.3.0:
resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==}
engines: {node: '>=6.11.5'}
@@ -1214,6 +1333,10 @@ packages:
is-unicode-supported: 0.1.0
dev: true
/longest-streak/3.0.1:
resolution: {integrity: sha512-cHlYSUpL2s7Fb3394mYxwTYj8niTaNHUCLr0qdiCXQfSjfuA7CKofpX2uSwEfFDQ0EB7JcnMnm+GjbqqoinYYg==}
dev: false
/loose-envify/1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
@@ -1226,10 +1349,389 @@ packages:
sourcemap-codec: 1.4.8
dev: true
/markdown-table/3.0.2:
resolution: {integrity: sha512-y8j3a5/DkJCmS5x4dMCQL+OR0+2EAq3DOtio1COSHsmW2BGXnNCK3v12hJt1LrUz5iZH5g0LmuYOjDdI+czghA==}
dev: false
/mdast-util-definitions/5.1.1:
resolution: {integrity: sha512-rQ+Gv7mHttxHOBx2dkF4HWTg+EE+UR78ptQWDylzPKaQuVGdG4HIoY3SrS/pCp80nZ04greFvXbVFHT+uf0JVQ==}
dependencies:
'@types/mdast': 3.0.10
'@types/unist': 2.0.6
unist-util-visit: 4.1.1
dev: false
/mdast-util-find-and-replace/2.2.1:
resolution: {integrity: sha512-SobxkQXFAdd4b5WmEakmkVoh18icjQRxGy5OWTCzgsLRm1Fu/KCtwD1HIQSsmq5ZRjVH0Ehwg6/Fn3xIUk+nKw==}
dependencies:
escape-string-regexp: 5.0.0
unist-util-is: 5.1.1
unist-util-visit-parents: 5.1.1
dev: false
/mdast-util-from-markdown/1.2.0:
resolution: {integrity: sha512-iZJyyvKD1+K7QX1b5jXdE7Sc5dtoTry1vzV28UZZe8Z1xVnB/czKntJ7ZAkG0tANqRnBF6p3p7GpU1y19DTf2Q==}
dependencies:
'@types/mdast': 3.0.10
'@types/unist': 2.0.6
decode-named-character-reference: 1.0.2
mdast-util-to-string: 3.1.0
micromark: 3.0.10
micromark-util-decode-numeric-character-reference: 1.0.0
micromark-util-decode-string: 1.0.2
micromark-util-normalize-identifier: 1.0.0
micromark-util-symbol: 1.0.1
micromark-util-types: 1.0.2
unist-util-stringify-position: 3.0.2
uvu: 0.5.6
transitivePeerDependencies:
- supports-color
dev: false
/mdast-util-gfm-autolink-literal/1.0.2:
resolution: {integrity: sha512-FzopkOd4xTTBeGXhXSBU0OCDDh5lUj2rd+HQqG92Ld+jL4lpUfgX2AT2OHAVP9aEeDKp7G92fuooSZcYJA3cRg==}
dependencies:
'@types/mdast': 3.0.10
ccount: 2.0.1
mdast-util-find-and-replace: 2.2.1
micromark-util-character: 1.1.0
dev: false
/mdast-util-gfm-footnote/1.0.1:
resolution: {integrity: sha512-p+PrYlkw9DeCRkTVw1duWqPRHX6Ywh2BNKJQcZbCwAuP/59B0Lk9kakuAd7KbQprVO4GzdW8eS5++A9PUSqIyw==}
dependencies:
'@types/mdast': 3.0.10
mdast-util-to-markdown: 1.3.0
micromark-util-normalize-identifier: 1.0.0
dev: false
/mdast-util-gfm-strikethrough/1.0.1:
resolution: {integrity: sha512-zKJbEPe+JP6EUv0mZ0tQUyLQOC+FADt0bARldONot/nefuISkaZFlmVK4tU6JgfyZGrky02m/I6PmehgAgZgqg==}
dependencies:
'@types/mdast': 3.0.10
mdast-util-to-markdown: 1.3.0
dev: false
/mdast-util-gfm-table/1.0.4:
resolution: {integrity: sha512-aEuoPwZyP4iIMkf2cLWXxx3EQ6Bmh2yKy9MVCg4i6Sd3cX80dcLEfXO/V4ul3pGH9czBK4kp+FAl+ZHmSUt9/w==}
dependencies:
markdown-table: 3.0.2
mdast-util-from-markdown: 1.2.0
mdast-util-to-markdown: 1.3.0
transitivePeerDependencies:
- supports-color
dev: false
/mdast-util-gfm-task-list-item/1.0.1:
resolution: {integrity: sha512-KZ4KLmPdABXOsfnM6JHUIjxEvcx2ulk656Z/4Balw071/5qgnhz+H1uGtf2zIGnrnvDC8xR4Fj9uKbjAFGNIeA==}
dependencies:
'@types/mdast': 3.0.10
mdast-util-to-markdown: 1.3.0
dev: false
/mdast-util-gfm/2.0.1:
resolution: {integrity: sha512-42yHBbfWIFisaAfV1eixlabbsa6q7vHeSPY+cg+BBjX51M8xhgMacqH9g6TftB/9+YkcI0ooV4ncfrJslzm/RQ==}
dependencies:
mdast-util-from-markdown: 1.2.0
mdast-util-gfm-autolink-literal: 1.0.2
mdast-util-gfm-footnote: 1.0.1
mdast-util-gfm-strikethrough: 1.0.1
mdast-util-gfm-table: 1.0.4
mdast-util-gfm-task-list-item: 1.0.1
mdast-util-to-markdown: 1.3.0
transitivePeerDependencies:
- supports-color
dev: false
/mdast-util-to-hast/12.2.1:
resolution: {integrity: sha512-dyindR2P7qOqXO1hQirZeGtVbiX7xlNQbw7gGaAwN4A1dh4+X8xU/JyYmRoyB8Fu1uPXzp7mlL5QwW7k+knvgA==}
dependencies:
'@types/hast': 2.3.4
'@types/mdast': 3.0.10
'@types/mdurl': 1.0.2
mdast-util-definitions: 5.1.1
mdurl: 1.0.1
micromark-util-sanitize-uri: 1.0.0
trim-lines: 3.0.1
unist-builder: 3.0.0
unist-util-generated: 2.0.0
unist-util-position: 4.0.3
unist-util-visit: 4.1.1
dev: false
/mdast-util-to-markdown/1.3.0:
resolution: {integrity: sha512-6tUSs4r+KK4JGTTiQ7FfHmVOaDrLQJPmpjD6wPMlHGUVXoG9Vjc3jIeP+uyBWRf8clwB2blM+W7+KrlMYQnftA==}
dependencies:
'@types/mdast': 3.0.10
'@types/unist': 2.0.6
longest-streak: 3.0.1
mdast-util-to-string: 3.1.0
micromark-util-decode-string: 1.0.2
unist-util-visit: 4.1.1
zwitch: 2.0.2
dev: false
/mdast-util-to-string/3.1.0:
resolution: {integrity: sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==}
dev: false
/mdurl/1.0.1:
resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==}
dev: false
/merge-stream/2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
dev: true
/micromark-core-commonmark/1.0.6:
resolution: {integrity: sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==}
dependencies:
decode-named-character-reference: 1.0.2
micromark-factory-destination: 1.0.0
micromark-factory-label: 1.0.2
micromark-factory-space: 1.0.0
micromark-factory-title: 1.0.2
micromark-factory-whitespace: 1.0.0
micromark-util-character: 1.1.0
micromark-util-chunked: 1.0.0
micromark-util-classify-character: 1.0.0
micromark-util-html-tag-name: 1.1.0
micromark-util-normalize-identifier: 1.0.0
micromark-util-resolve-all: 1.0.0
micromark-util-subtokenize: 1.0.2
micromark-util-symbol: 1.0.1
micromark-util-types: 1.0.2
uvu: 0.5.6
dev: false
/micromark-extension-gfm-autolink-literal/1.0.3:
resolution: {integrity: sha512-i3dmvU0htawfWED8aHMMAzAVp/F0Z+0bPh3YrbTPPL1v4YAlCZpy5rBO5p0LPYiZo0zFVkoYh7vDU7yQSiCMjg==}
dependencies:
micromark-util-character: 1.1.0
micromark-util-sanitize-uri: 1.0.0
micromark-util-symbol: 1.0.1
micromark-util-types: 1.0.2
uvu: 0.5.6
dev: false
/micromark-extension-gfm-footnote/1.0.4:
resolution: {integrity: sha512-E/fmPmDqLiMUP8mLJ8NbJWJ4bTw6tS+FEQS8CcuDtZpILuOb2kjLqPEeAePF1djXROHXChM/wPJw0iS4kHCcIg==}
dependencies:
micromark-core-commonmark: 1.0.6
micromark-factory-space: 1.0.0
micromark-util-character: 1.1.0
micromark-util-normalize-identifier: 1.0.0
micromark-util-sanitize-uri: 1.0.0
micromark-util-symbol: 1.0.1
micromark-util-types: 1.0.2
uvu: 0.5.6
dev: false
/micromark-extension-gfm-strikethrough/1.0.4:
resolution: {integrity: sha512-/vjHU/lalmjZCT5xt7CcHVJGq8sYRm80z24qAKXzaHzem/xsDYb2yLL+NNVbYvmpLx3O7SYPuGL5pzusL9CLIQ==}
dependencies:
micromark-util-chunked: 1.0.0
micromark-util-classify-character: 1.0.0
micromark-util-resolve-all: 1.0.0
micromark-util-symbol: 1.0.1
micromark-util-types: 1.0.2
uvu: 0.5.6
dev: false
/micromark-extension-gfm-table/1.0.5:
resolution: {integrity: sha512-xAZ8J1X9W9K3JTJTUL7G6wSKhp2ZYHrFk5qJgY/4B33scJzE2kpfRL6oiw/veJTbt7jiM/1rngLlOKPWr1G+vg==}
dependencies:
micromark-factory-space: 1.0.0
micromark-util-character: 1.1.0
micromark-util-symbol: 1.0.1
micromark-util-types: 1.0.2
uvu: 0.5.6
dev: false
/micromark-extension-gfm-tagfilter/1.0.1:
resolution: {integrity: sha512-Ty6psLAcAjboRa/UKUbbUcwjVAv5plxmpUTy2XC/3nJFL37eHej8jrHrRzkqcpipJliuBH30DTs7+3wqNcQUVA==}
dependencies:
micromark-util-types: 1.0.2
dev: false
/micromark-extension-gfm-task-list-item/1.0.3:
resolution: {integrity: sha512-PpysK2S1Q/5VXi72IIapbi/jliaiOFzv7THH4amwXeYXLq3l1uo8/2Be0Ac1rEwK20MQEsGH2ltAZLNY2KI/0Q==}
dependencies:
micromark-factory-space: 1.0.0
micromark-util-character: 1.1.0
micromark-util-symbol: 1.0.1
micromark-util-types: 1.0.2
uvu: 0.5.6
dev: false
/micromark-extension-gfm/2.0.1:
resolution: {integrity: sha512-p2sGjajLa0iYiGQdT0oelahRYtMWvLjy8J9LOCxzIQsllMCGLbsLW+Nc+N4vi02jcRJvedVJ68cjelKIO6bpDA==}
dependencies:
micromark-extension-gfm-autolink-literal: 1.0.3
micromark-extension-gfm-footnote: 1.0.4
micromark-extension-gfm-strikethrough: 1.0.4
micromark-extension-gfm-table: 1.0.5
micromark-extension-gfm-tagfilter: 1.0.1
micromark-extension-gfm-task-list-item: 1.0.3
micromark-util-combine-extensions: 1.0.0
micromark-util-types: 1.0.2
dev: false
/micromark-factory-destination/1.0.0:
resolution: {integrity: sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==}
dependencies:
micromark-util-character: 1.1.0
micromark-util-symbol: 1.0.1
micromark-util-types: 1.0.2
dev: false
/micromark-factory-label/1.0.2:
resolution: {integrity: sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==}
dependencies:
micromark-util-character: 1.1.0
micromark-util-symbol: 1.0.1
micromark-util-types: 1.0.2
uvu: 0.5.6
dev: false
/micromark-factory-space/1.0.0:
resolution: {integrity: sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==}
dependencies:
micromark-util-character: 1.1.0
micromark-util-types: 1.0.2
dev: false
/micromark-factory-title/1.0.2:
resolution: {integrity: sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==}
dependencies:
micromark-factory-space: 1.0.0
micromark-util-character: 1.1.0
micromark-util-symbol: 1.0.1
micromark-util-types: 1.0.2
uvu: 0.5.6
dev: false
/micromark-factory-whitespace/1.0.0:
resolution: {integrity: sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==}
dependencies:
micromark-factory-space: 1.0.0
micromark-util-character: 1.1.0
micromark-util-symbol: 1.0.1
micromark-util-types: 1.0.2
dev: false
/micromark-util-character/1.1.0:
resolution: {integrity: sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==}
dependencies:
micromark-util-symbol: 1.0.1
micromark-util-types: 1.0.2
dev: false
/micromark-util-chunked/1.0.0:
resolution: {integrity: sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==}
dependencies:
micromark-util-symbol: 1.0.1
dev: false
/micromark-util-classify-character/1.0.0:
resolution: {integrity: sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==}
dependencies:
micromark-util-character: 1.1.0
micromark-util-symbol: 1.0.1
micromark-util-types: 1.0.2
dev: false
/micromark-util-combine-extensions/1.0.0:
resolution: {integrity: sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==}
dependencies:
micromark-util-chunked: 1.0.0
micromark-util-types: 1.0.2
dev: false
/micromark-util-decode-numeric-character-reference/1.0.0:
resolution: {integrity: sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==}
dependencies:
micromark-util-symbol: 1.0.1
dev: false
/micromark-util-decode-string/1.0.2:
resolution: {integrity: sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==}
dependencies:
decode-named-character-reference: 1.0.2
micromark-util-character: 1.1.0
micromark-util-decode-numeric-character-reference: 1.0.0
micromark-util-symbol: 1.0.1
dev: false
/micromark-util-encode/1.0.1:
resolution: {integrity: sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA==}
dev: false
/micromark-util-html-tag-name/1.1.0:
resolution: {integrity: sha512-BKlClMmYROy9UiV03SwNmckkjn8QHVaWkqoAqzivabvdGcwNGMMMH/5szAnywmsTBUzDsU57/mFi0sp4BQO6dA==}
dev: false
/micromark-util-normalize-identifier/1.0.0:
resolution: {integrity: sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==}
dependencies:
micromark-util-symbol: 1.0.1
dev: false
/micromark-util-resolve-all/1.0.0:
resolution: {integrity: sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==}
dependencies:
micromark-util-types: 1.0.2
dev: false
/micromark-util-sanitize-uri/1.0.0:
resolution: {integrity: sha512-cCxvBKlmac4rxCGx6ejlIviRaMKZc0fWm5HdCHEeDWRSkn44l6NdYVRyU+0nT1XC72EQJMZV8IPHF+jTr56lAg==}
dependencies:
micromark-util-character: 1.1.0
micromark-util-encode: 1.0.1
micromark-util-symbol: 1.0.1
dev: false
/micromark-util-subtokenize/1.0.2:
resolution: {integrity: sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==}
dependencies:
micromark-util-chunked: 1.0.0
micromark-util-symbol: 1.0.1
micromark-util-types: 1.0.2
uvu: 0.5.6
dev: false
/micromark-util-symbol/1.0.1:
resolution: {integrity: sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ==}
dev: false
/micromark-util-types/1.0.2:
resolution: {integrity: sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==}
dev: false
/micromark/3.0.10:
resolution: {integrity: sha512-ryTDy6UUunOXy2HPjelppgJ2sNfcPz1pLlMdA6Rz9jPzhLikWXv/irpWV/I2jd68Uhmny7hHxAlAhk4+vWggpg==}
dependencies:
'@types/debug': 4.1.7
debug: 4.3.4
decode-named-character-reference: 1.0.2
micromark-core-commonmark: 1.0.6
micromark-factory-space: 1.0.0
micromark-util-character: 1.1.0
micromark-util-chunked: 1.0.0
micromark-util-combine-extensions: 1.0.0
micromark-util-decode-numeric-character-reference: 1.0.0
micromark-util-encode: 1.0.1
micromark-util-normalize-identifier: 1.0.0
micromark-util-resolve-all: 1.0.0
micromark-util-sanitize-uri: 1.0.0
micromark-util-subtokenize: 1.0.2
micromark-util-symbol: 1.0.1
micromark-util-types: 1.0.2
uvu: 0.5.6
transitivePeerDependencies:
- supports-color
dev: false
/mime-db/1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
@@ -1253,9 +1755,17 @@ packages:
brace-expansion: 1.1.11
dev: true
/minimist/1.2.6:
resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==}
dev: false
/mri/1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
dev: false
/ms/2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: true
/mute-stream/0.0.8:
resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==}
@@ -1359,6 +1869,10 @@ packages:
object-assign: 4.1.1
react-is: 16.13.1
/property-information/6.1.1:
resolution: {integrity: sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w==}
dev: false
/punycode/2.1.1:
resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==}
engines: {node: '>=6'}
@@ -1393,6 +1907,37 @@ packages:
/react-is/16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
/react-is/18.2.0:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
dev: false
/react-markdown/8.0.3_vshvapmxg47tngu7tvrsqpq55u:
resolution: {integrity: sha512-We36SfqaKoVNpN1QqsZwWSv/OZt5J15LNgTLWynwAN5b265hrQrsjMtlRNwUvS+YyR3yDM8HpTNc4pK9H/Gc0A==}
peerDependencies:
'@types/react': '>=16'
react: '>=16'
dependencies:
'@types/hast': 2.3.4
'@types/prop-types': 15.7.5
'@types/react': 16.14.0
'@types/unist': 2.0.6
comma-separated-tokens: 2.0.2
hast-util-whitespace: 2.0.0
prop-types: 15.8.1
property-information: 6.1.1
react: 16.14.0
react-is: 18.2.0
remark-parse: 10.0.1
remark-rehype: 10.1.0
space-separated-tokens: 2.0.1
style-to-object: 0.3.0
unified: 10.1.2
unist-util-visit: 4.1.1
vfile: 5.3.4
transitivePeerDependencies:
- supports-color
dev: false
/react/16.14.0:
resolution: {integrity: sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==}
engines: {node: '>=0.10.0'}
@@ -1410,6 +1955,36 @@ packages:
util-deprecate: 1.0.2
dev: true
/remark-gfm/3.0.1:
resolution: {integrity: sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==}
dependencies:
'@types/mdast': 3.0.10
mdast-util-gfm: 2.0.1
micromark-extension-gfm: 2.0.1
unified: 10.1.2
transitivePeerDependencies:
- supports-color
dev: false
/remark-parse/10.0.1:
resolution: {integrity: sha512-1fUyHr2jLsVOkhbvPRBJ5zTKZZyD6yZzYaWCS6BPBdQ8vEMBCH+9zNCDA6tET/zHCi/jLqjCWtlJZUPk+DbnFw==}
dependencies:
'@types/mdast': 3.0.10
mdast-util-from-markdown: 1.2.0
unified: 10.1.2
transitivePeerDependencies:
- supports-color
dev: false
/remark-rehype/10.1.0:
resolution: {integrity: sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==}
dependencies:
'@types/hast': 2.3.4
'@types/mdast': 3.0.10
mdast-util-to-hast: 12.2.1
unified: 10.1.2
dev: false
/resolve-from/3.0.0:
resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==}
engines: {node: '>=4'}
@@ -1437,6 +2012,27 @@ packages:
signal-exit: 3.0.7
dev: true
/rollup-plugin-external-globals/0.6.1_rollup@2.76.0:
resolution: {integrity: sha512-mlp3KNa5sE4Sp9UUR2rjBrxjG79OyZAh/QC18RHIjM+iYkbBwNXSo8DHRMZWtzJTrH8GxQ+SJvCTN3i14uMXIA==}
peerDependencies:
rollup: ^2.25.0
dependencies:
'@rollup/pluginutils': 4.2.1
estree-walker: 2.0.2
is-reference: 1.2.1
magic-string: 0.25.9
rollup: 2.76.0
dev: true
/rollup-plugin-polyfill-node/0.10.2_rollup@2.76.0:
resolution: {integrity: sha512-5GMywXiLiuQP6ZzED/LO/Q0HyDi2W6b8VN+Zd3oB0opIjyRs494Me2ZMaqKWDNbGiW4jvvzl6L2n4zRgxS9cSQ==}
peerDependencies:
rollup: ^1.20.0 || ^2.0.0
dependencies:
'@rollup/plugin-inject': 4.0.4_rollup@2.76.0
rollup: 2.76.0
dev: true
/rollup/2.76.0:
resolution: {integrity: sha512-9jwRIEY1jOzKLj3nsY/yot41r19ITdQrhs+q3ggNWhr9TQgduHqANvPpS32RNpzGklJu3G1AJfvlZLi/6wFgWA==}
engines: {node: '>=10.0.0'}
@@ -1456,6 +2052,13 @@ packages:
tslib: 2.4.0
dev: true
/sade/1.8.1:
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
engines: {node: '>=6'}
dependencies:
mri: 1.2.0
dev: false
/safe-buffer/5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
dev: true
@@ -1515,6 +2118,10 @@ packages:
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
dev: true
/space-separated-tokens/2.0.1:
resolution: {integrity: sha512-ekwEbFp5aqSPKaqeY1PGrlGQxPNaq+Cnx4+bE2D8sciBQrHpbwoBbawqTN2+6jPs9IdWxxiUcN0K2pkczD3zmw==}
dev: false
/sprintf-js/1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
dev: true
@@ -1541,6 +2148,12 @@ packages:
ansi-regex: 5.0.1
dev: true
/style-to-object/0.3.0:
resolution: {integrity: sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==}
dependencies:
inline-style-parser: 0.1.1
dev: false
/supports-color/5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
@@ -1623,6 +2236,14 @@ packages:
engines: {node: '>=4'}
dev: true
/trim-lines/3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
dev: false
/trough/2.1.0:
resolution: {integrity: sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==}
dev: false
/tslib/2.4.0:
resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==}
dev: true
@@ -1644,6 +2265,59 @@ packages:
hasBin: true
dev: true
/unified/10.1.2:
resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==}
dependencies:
'@types/unist': 2.0.6
bail: 2.0.2
extend: 3.0.2
is-buffer: 2.0.5
is-plain-obj: 4.1.0
trough: 2.1.0
vfile: 5.3.4
dev: false
/unist-builder/3.0.0:
resolution: {integrity: sha512-GFxmfEAa0vi9i5sd0R2kcrI9ks0r82NasRq5QHh2ysGngrc6GiqD5CDf1FjPenY4vApmFASBIIlk/jj5J5YbmQ==}
dependencies:
'@types/unist': 2.0.6
dev: false
/unist-util-generated/2.0.0:
resolution: {integrity: sha512-TiWE6DVtVe7Ye2QxOVW9kqybs6cZexNwTwSMVgkfjEReqy/xwGpAXb99OxktoWwmL+Z+Epb0Dn8/GNDYP1wnUw==}
dev: false
/unist-util-is/5.1.1:
resolution: {integrity: sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ==}
dev: false
/unist-util-position/4.0.3:
resolution: {integrity: sha512-p/5EMGIa1qwbXjA+QgcBXaPWjSnZfQ2Sc3yBEEfgPwsEmJd8Qh+DSk3LGnmOM4S1bY2C0AjmMnB8RuEYxpPwXQ==}
dependencies:
'@types/unist': 2.0.6
dev: false
/unist-util-stringify-position/3.0.2:
resolution: {integrity: sha512-7A6eiDCs9UtjcwZOcCpM4aPII3bAAGv13E96IkawkOAW0OhH+yRxtY0lzo8KiHpzEMfH7Q+FizUmwp8Iqy5EWg==}
dependencies:
'@types/unist': 2.0.6
dev: false
/unist-util-visit-parents/5.1.1:
resolution: {integrity: sha512-gks4baapT/kNRaWxuGkl5BIhoanZo7sC/cUT/JToSRNL1dYoXRFl75d++NkjYk4TAu2uv2Px+l8guMajogeuiw==}
dependencies:
'@types/unist': 2.0.6
unist-util-is: 5.1.1
dev: false
/unist-util-visit/4.1.1:
resolution: {integrity: sha512-n9KN3WV9k4h1DxYR1LoajgN93wpEi/7ZplVe02IoB4gH5ctI1AaF2670BLHQYbwj+pY83gFtyeySFiyMHJklrg==}
dependencies:
'@types/unist': 2.0.6
unist-util-is: 5.1.1
unist-util-visit-parents: 5.1.1
dev: false
/update-browserslist-db/1.0.4_browserslist@4.21.2:
resolution: {integrity: sha512-jnmO2BEGUjsMOe/Fg9u0oczOe/ppIDZPebzccl1yDWGLFP16Pa1/RM5wEoKYPG2zstNcDuAStejyxsOuKINdGA==}
hasBin: true
@@ -1665,6 +2339,33 @@ packages:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true
/uvu/0.5.6:
resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==}
engines: {node: '>=8'}
hasBin: true
dependencies:
dequal: 2.0.3
diff: 5.1.0
kleur: 4.1.5
sade: 1.8.1
dev: false
/vfile-message/3.1.2:
resolution: {integrity: sha512-QjSNP6Yxzyycd4SVOtmKKyTsSvClqBPJcd00Z0zuPj3hOIjg0rUPG6DbFGPvUKRgYyaIWLPKpuEclcuvb3H8qA==}
dependencies:
'@types/unist': 2.0.6
unist-util-stringify-position: 3.0.2
dev: false
/vfile/5.3.4:
resolution: {integrity: sha512-KI+7cnst03KbEyN1+JE504zF5bJBZa+J+CrevLeyIMq0aPU681I2rQ5p4PlnQ6exFtWiUrg26QUdFMnAKR6PIw==}
dependencies:
'@types/unist': 2.0.6
is-buffer: 2.0.5
unist-util-stringify-position: 3.0.2
vfile-message: 3.1.2
dev: false
/watchpack/2.4.0:
resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==}
engines: {node: '>=10.13.0'}
@@ -1736,3 +2437,7 @@ packages:
/wrappy/1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
dev: true
/zwitch/2.0.2:
resolution: {integrity: sha512-JZxotl7SxAJH0j7dN4pxsTV6ZLXoLdGME+PsjkL/DaBrVryK9kTGq06GfKrwcSOqypP+fdXGoCHE36b99fWVoA==}
dev: false
+16 -8
View File
@@ -1,6 +1,7 @@
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import externalGlobals from "rollup-plugin-external-globals";
import replace from '@rollup/plugin-replace';
import typescript from '@rollup/plugin-typescript';
import { defineConfig } from 'rollup';
@@ -10,6 +11,14 @@ export default defineConfig({
plugins: [
commonjs(),
nodeResolve(),
externalGlobals({
react: 'SP_REACT',
'react-dom': 'SP_REACTDOM',
// hack to shut up react-markdown
'process': '{cwd: () => {}}',
'path': '{dirname: () => {}, join: () => {}, basename: () => {}, extname: () => {}}',
'url': '{fileURLToPath: (f) => f}'
}),
typescript(),
json(),
replace({
@@ -17,13 +26,12 @@ export default defineConfig({
'process.env.NODE_ENV': JSON.stringify('production'),
}),
],
external: ["react", "react-dom"],
preserveEntrySignatures: false,
output: {
file: '../backend/static/plugin-loader.iife.js',
globals: {
react: 'SP_REACT',
'react-dom': 'SP_REACTDOM',
},
format: 'iife',
},
dir: '../backend/static',
format: 'esm',
chunkFileNames: (chunkInfo) => {
return 'chunk-[hash].js'
}
}
});
@@ -80,7 +80,6 @@ export const DeckyRouterStateContextProvider: FC<Props> = ({ children, deckyRout
useEffect(() => {
function onUpdate() {
console.log('test', deckyRouterState.publicState());
setPublicDeckyRouterState({ ...deckyRouterState.publicState() });
}
+34 -2
View File
@@ -1,20 +1,33 @@
import { FC, createContext, useContext, useEffect, useState } from 'react';
import { Plugin } from '../plugin';
import { PluginUpdateMapping } from '../store';
interface PublicDeckyState {
plugins: Plugin[];
activePlugin: Plugin | null;
updates: PluginUpdateMapping | null;
hasLoaderUpdate?: boolean;
isLoaderUpdating: boolean;
}
export class DeckyState {
private _plugins: Plugin[] = [];
private _activePlugin: Plugin | null = null;
private _updates: PluginUpdateMapping | null = null;
private _hasLoaderUpdate: boolean = false;
private _isLoaderUpdating: boolean = false;
public eventBus = new EventTarget();
publicState(): PublicDeckyState {
return { plugins: this._plugins, activePlugin: this._activePlugin };
return {
plugins: this._plugins,
activePlugin: this._activePlugin,
updates: this._updates,
hasLoaderUpdate: this._hasLoaderUpdate,
isLoaderUpdating: this._isLoaderUpdating,
};
}
setPlugins(plugins: Plugin[]) {
@@ -32,12 +45,28 @@ export class DeckyState {
this.notifyUpdate();
}
setUpdates(updates: PluginUpdateMapping) {
this._updates = updates;
this.notifyUpdate();
}
setHasLoaderUpdate(hasUpdate: boolean) {
this._hasLoaderUpdate = hasUpdate;
this.notifyUpdate();
}
setIsLoaderUpdating(isUpdating: boolean) {
this._isLoaderUpdating = isUpdating;
this.notifyUpdate();
}
private notifyUpdate() {
this.eventBus.dispatchEvent(new Event('update'));
}
}
interface DeckyStateContext extends PublicDeckyState {
setIsLoaderUpdating(hasUpdate: boolean): void;
setActivePlugin(name: string): void;
closeActivePlugin(): void;
}
@@ -63,11 +92,14 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
return () => deckyState.eventBus.removeEventListener('update', onUpdate);
}, []);
const setIsLoaderUpdating = (hasUpdate: boolean) => deckyState.setIsLoaderUpdating(hasUpdate);
const setActivePlugin = (name: string) => deckyState.setActivePlugin(name);
const closeActivePlugin = () => deckyState.closeActivePlugin();
return (
<DeckyStateContext.Provider value={{ ...publicDeckyState, setActivePlugin, closeActivePlugin }}>
<DeckyStateContext.Provider
value={{ ...publicDeckyState, setIsLoaderUpdating, setActivePlugin, closeActivePlugin }}
>
{children}
</DeckyStateContext.Provider>
);
+9
View File
@@ -0,0 +1,9 @@
import { FunctionComponent } from 'react';
import ReactMarkdown, { Options as ReactMarkdownOptions } from 'react-markdown';
import remarkGfm from 'remark-gfm';
const Markdown: FunctionComponent<ReactMarkdownOptions> = (props) => {
return <ReactMarkdown remarkPlugins={[remarkGfm]} {...props} />;
};
export default Markdown;
@@ -0,0 +1,25 @@
import { CSSProperties, FunctionComponent } from 'react';
interface NotificationBadgeProps {
show?: boolean;
style?: CSSProperties;
}
const NotificationBadge: FunctionComponent<NotificationBadgeProps> = ({ show, style }) => {
return show ? (
<div
style={{
position: 'absolute',
top: '8px',
right: '8px',
height: '10px',
width: '10px',
background: 'orange',
borderRadius: '50%',
...style,
}}
/>
) : null;
};
export default NotificationBadge;
+35 -18
View File
@@ -1,30 +1,47 @@
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';
import NotificationBadge from './NotificationBadge';
const PluginView: VFC = () => {
const { plugins, activePlugin, setActivePlugin } = useDeckyState();
const { plugins, updates, 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>
<NotificationBadge show={updates?.has(name)} style={{ top: '-5px', right: '-5px' }} />
</div>
</ButtonItem>
</PanelSectionRow>
))}
</PanelSection>
</div>
);
};
-2
View File
@@ -7,9 +7,7 @@ import { useDeckyState } from './DeckyState';
const titleStyles: CSSProperties = {
display: 'flex',
paddingTop: '3px',
paddingBottom: '14px',
paddingRight: '16px',
boxShadow: 'unset',
};
const TitleView: VFC = () => {
+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;
@@ -1,4 +1,4 @@
import { ModalRoot, QuickAccessTab, Router, Spinner, sleep, staticClasses } from 'decky-frontend-lib';
import { ModalRoot, QuickAccessTab, Router, Spinner, staticClasses } from 'decky-frontend-lib';
import { FC, useState } from 'react';
interface PluginInstallModalProps {
@@ -20,9 +20,7 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, ha
onOK={async () => {
setLoading(true);
await onOK();
Router.NavigateBackOrOpenMenu();
await sleep(250);
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 1000);
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 250);
}}
onCancel={async () => {
await onCancel();
@@ -0,0 +1,21 @@
import { Focusable, updaterFieldClasses } from 'decky-frontend-lib';
import { FunctionComponent, ReactNode } from 'react';
interface InlinePatchNotesProps {
date: ReactNode;
title: string;
children: ReactNode;
onClick?: () => void;
}
const InlinePatchNotes: FunctionComponent<InlinePatchNotesProps> = ({ date, title, children, onClick }) => {
return (
<Focusable className={updaterFieldClasses.PatchNotes} onActivate={onClick}>
<div className={updaterFieldClasses.PostedTime}>{date}</div>
<div className={updaterFieldClasses.EventDetailTitle}>{title}</div>
<div className={updaterFieldClasses.EventDetailsBody}>{children}</div>
</Focusable>
);
};
export default InlinePatchNotes;
@@ -0,0 +1,33 @@
import { Dropdown, Field } from 'decky-frontend-lib';
import { FunctionComponent } from 'react';
import { useSetting } from '../../../../utils/hooks/useSetting';
enum UpdateBranch {
Stable,
Prerelease,
Nightly,
}
const BranchSelect: FunctionComponent<{}> = () => {
const [selectedBranch, setSelectedBranch] = useSetting<UpdateBranch>('branch', UpdateBranch.Prerelease);
return (
<Field label="Update Channel">
<Dropdown
rgOptions={Object.values(UpdateBranch)
.filter((branch) => typeof branch == 'string')
.map((branch) => ({
label: branch,
data: UpdateBranch[branch],
}))}
selectedOption={selectedBranch}
onChange={(newVal) => {
setSelectedBranch(newVal.data);
}}
/>
</Field>
);
};
export default BranchSelect;
@@ -0,0 +1,29 @@
import { Field, Toggle } from 'decky-frontend-lib';
import { FaBug } from 'react-icons/fa';
import { useSetting } from '../../../../utils/hooks/useSetting';
export default function RemoteDebuggingSettings() {
const [allowRemoteDebugging, setAllowRemoteDebugging] = useSetting<boolean>('cef_forward', false);
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' }} />}
>
<Toggle
value={allowRemoteDebugging || false}
onChange={(toggleValue) => {
setAllowRemoteDebugging(toggleValue);
if (toggleValue) window.DeckyPluginLoader.callServerMethod('allow_remote_debugging');
else window.DeckyPluginLoader.callServerMethod('disallow_remote_debugging');
}}
/>
</Field>
);
}
@@ -1,30 +1,67 @@
import { DialogButton, Field, ProgressBarWithInfo, Spinner } from 'decky-frontend-lib';
import { Carousel, DialogButton, Field, Focusable, ProgressBarWithInfo, Spinner, showModal } from 'decky-frontend-lib';
import { useCallback } from 'react';
import { Suspense, lazy } from 'react';
import { useEffect, useState } from 'react';
import { FaArrowDown } from 'react-icons/fa';
import { callUpdaterMethod, finishUpdate } from '../../../../updater';
import { VerInfo, callUpdaterMethod, finishUpdate } from '../../../../updater';
import { useDeckyState } from '../../../DeckyState';
import InlinePatchNotes from '../../../patchnotes/InlinePatchNotes';
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;
const MarkdownRenderer = lazy(() => import('../../../Markdown'));
// import ReactMarkdown from 'react-markdown'
// import remarkGfm from 'remark-gfm'
function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | null; closeModal?: () => {} }) {
return (
<Focusable onCancelButton={closeModal}>
<Carousel
fnItemRenderer={(id: number) => (
<Focusable
onActivate={() => {}}
style={{
marginTop: '40px',
height: 'calc( 100% - 40px )',
overflowY: 'scroll',
display: 'flex',
justifyContent: 'center',
margin: '40px',
}}
>
<div>
<h1>{versionInfo?.all?.[id]?.name}</h1>
{versionInfo?.all?.[id]?.body ? (
<Suspense fallback={<Spinner style={{ width: '24', height: '24' }} />}>
<MarkdownRenderer>{versionInfo.all[id].body}</MarkdownRenderer>
</Suspense>
) : (
'no patch notes for this version'
)}
</div>
</Focusable>
)}
fnGetId={(id) => id}
nNumItems={versionInfo?.all?.length}
nHeight={window.innerHeight - 150}
nItemHeight={window.innerHeight - 200}
nItemMarginX={0}
initialColumn={0}
autoFocus={true}
fnGetColumnWidth={() => window.innerWidth}
/>
</Focusable>
);
}
export default function UpdaterSettings() {
const { isLoaderUpdating, setIsLoaderUpdating } = useDeckyState();
const [versionInfo, setVersionInfo] = useState<VerInfo | null>(null);
const [checkingForUpdates, setCheckingForUpdates] = useState<boolean>(false);
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 };
@@ -32,66 +69,94 @@ export default function UpdaterSettings() {
})();
}, []);
useEffect(() => {
window.DeckyUpdater = {
updateProgress: (i) => {
setUpdateProgress(i);
setIsLoaderUpdating(true);
},
finish: async () => {
setUpdateProgress(0);
setReloading(true);
await finishUpdate();
},
};
}, []);
const showPatchNotes = useCallback(() => {
showModal(<PatchNotesModal versionInfo={versionInfo} />);
}, [versionInfo]);
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' }} />
<>
<Field
onOptionsActionDescription={versionInfo?.all ? 'Patch Notes' : undefined}
onOptionsButton={versionInfo?.all ? showPatchNotes : undefined}
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 && !isLoaderUpdating ? (
<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 () => {
setUpdateProgress(0);
callUpdaterMethod('do_update');
}
}
>
{checkingForUpdates
? 'Checking'
: !versionInfo?.remote || versionInfo?.remote?.tag_name == versionInfo?.current
? 'Check For Updates'
: 'Install Update'}
</DialogButton>
) : (
<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');
}
}
<ProgressBarWithInfo
layout="inline"
bottomSeparator={false}
nProgress={updateProgress}
indeterminate={reloading}
sOperationText={reloading ? 'Reloading' : 'Updating'}
/>
)}
</Field>
{versionInfo?.remote && (
<InlinePatchNotes
title={versionInfo?.remote.name}
date={new Intl.RelativeTimeFormat('en-US', {
numeric: 'auto',
}).format(
Math.ceil((new Date(versionInfo.remote.published_at).getTime() - new Date().getTime()) / 86400000),
'day',
)}
onClick={showPatchNotes}
>
{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'}
/>
<Suspense fallback={<Spinner style={{ width: '24', height: '24' }} />}>
<MarkdownRenderer>{versionInfo?.remote.body}</MarkdownRenderer>
</Suspense>
</InlinePatchNotes>
)}
</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';
// import BranchSelect from './BranchSelect';
import RemoteDebuggingSettings from './RemoteDebugging';
import UpdaterSettings from './Updater';
export default function GeneralSettings() {
@@ -20,6 +22,8 @@ export default function GeneralSettings() {
/>
</Field> */}
<UpdaterSettings />
{/* <BranchSelect /> */}
<RemoteDebuggingSettings />
<Field
label="Manual plugin install"
description={<TextField label={'URL'} value={pluginURL} onChange={(e) => setPluginURL(e?.target.value)} />}
@@ -1,10 +1,16 @@
import { DialogButton, staticClasses } from 'decky-frontend-lib';
import { FaTrash } from 'react-icons/fa';
import { DialogButton, Focusable, Menu, MenuItem, showContextMenu } from 'decky-frontend-lib';
import { useEffect } from 'react';
import { FaDownload, FaEllipsisH } from 'react-icons/fa';
import { requestPluginInstall } from '../../../../store';
import { useDeckyState } from '../../../DeckyState';
export default function PluginList() {
const { plugins } = useDeckyState();
const { plugins, updates } = useDeckyState();
useEffect(() => {
window.DeckyPluginLoader.checkPluginUpdates();
}, []);
if (plugins.length === 0) {
return (
@@ -16,19 +22,45 @@ export default function PluginList() {
return (
<ul style={{ listStyleType: 'none' }}>
{plugins.map(({ name }) => (
<li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<span>{name}</span>
<div className={staticClasses.Title} style={{ marginLeft: 'auto', boxShadow: 'none' }}>
<DialogButton
style={{ height: '40px', width: '40px', padding: '10px 12px' }}
onClick={() => window.DeckyPluginLoader.uninstallPlugin(name)}
>
<FaTrash />
</DialogButton>
</div>
</li>
))}
{plugins.map(({ name, version }) => {
const update = updates?.get(name);
return (
<li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', paddingBottom: '10px' }}>
<span>
{name} {version}
</span>
<Focusable style={{ marginLeft: 'auto', boxShadow: 'none', display: 'flex', justifyContent: 'right' }}>
{update && (
<DialogButton
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
onClick={() => requestPluginInstall(name, update)}
>
<div style={{ display: 'flex', flexDirection: 'row' }}>
Update to {update.name}
<FaDownload style={{ paddingLeft: '2rem' }} />
</div>
</DialogButton>
)}
<DialogButton
style={{ height: '40px', width: '40px', padding: '10px 12px', minWidth: '40px' }}
onClick={(e: MouseEvent) =>
showContextMenu(
<Menu label="Plugin Actions">
<MenuItem onSelected={() => window.DeckyPluginLoader.importPlugin(name, version)}>
Reload
</MenuItem>
<MenuItem onSelected={() => window.DeckyPluginLoader.uninstallPlugin(name)}>Uninstall</MenuItem>
</Menu>,
e.currentTarget ?? window,
)
}
>
<FaEllipsisH />
</DialogButton>
</Focusable>
</li>
);
})}
</ul>
);
}
+32 -20
View File
@@ -6,6 +6,7 @@ import {
Router,
SingleDropdownOption,
SuspensefulImage,
joinClassNames,
staticClasses,
} from 'decky-frontend-lib';
import { FC, useRef, useState } from 'react';
@@ -14,22 +15,15 @@ import {
LegacyStorePlugin,
StorePlugin,
StorePluginVersion,
isLegacyPlugin,
requestLegacyPluginInstall,
requestPluginInstall,
} from './Store';
} from '../../store';
interface PluginCardProps {
plugin: StorePlugin | LegacyStorePlugin;
}
const classNames = (...classes: string[]) => {
return classes.join(' ');
};
function isLegacyPlugin(plugin: LegacyStorePlugin | StorePlugin): plugin is LegacyStorePlugin {
return 'artifact' in plugin;
}
const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
const [selectedOption, setSelectedOption] = useState<number>(0);
const buttonRef = useRef<HTMLDivElement>(null);
@@ -44,7 +38,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 +62,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 +85,10 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
display: 'flex',
flexDirection: 'row',
}}
className="deckyStoreCardBody"
>
<SuspensefulImage
className="deckyStoreCardImage"
suspenseWidth="256px"
style={{
width: 'auto',
@@ -113,17 +111,26 @@ 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)}>
<span>Tags:</span>
<p
className={joinClassNames('deckyStoreCardTagsContainer', staticClasses.PanelSectionRow)}
style={{
padding: '0 16px',
display: 'flex',
flexWrap: 'wrap',
gap: '5px 10px',
}}
>
<span style={{ padding: '5px 0' }}>Tags:</span>
{plugin.tags.map((tag: string) => (
<span
className="deckyStoreCardTag"
style={{
padding: '5px',
marginRight: '10px',
borderRadius: '5px',
background: tag == 'root' ? '#842029' : '#ACB2C947',
}}
@@ -133,10 +140,10 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
))}
{isLegacyPlugin(plugin) && (
<span
className="deckyStoreCardTag deckyStoreCardLegacyTag"
style={{
color: '#232120',
padding: '5px',
marginRight: '10px',
borderRadius: '5px',
background: '#EDE841',
}}
@@ -148,6 +155,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
</div>
</div>
<div
className="deckyStoreCardActionsContainer"
style={{
width: '100%',
alignSelf: 'flex-end',
@@ -156,6 +164,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
}}
>
<Focusable
className="deckyStoreCardActions"
style={{
display: 'flex',
flexDirection: 'row',
@@ -163,22 +172,25 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
}}
>
<div
className="deckyStoreCardInstallButtonContainer"
style={{
flex: '1',
}}
>
<DialogButton
className="deckyStoreCardInstallButton"
ref={buttonRef}
onClick={() =>
isLegacyPlugin(plugin)
? requestLegacyPluginInstall(plugin, Object.keys(plugin.versions)[selectedOption])
: requestPluginInstall(plugin, plugin.versions[selectedOption])
: requestPluginInstall(plugin.name, plugin.versions[selectedOption])
}
>
Install
</DialogButton>
</div>
<div
className="deckyStoreCardVersionDropdownContainer"
style={{
flex: '0.2',
}}
+5 -79
View File
@@ -1,95 +1,21 @@
import { ModalRoot, SteamSpinner, showModal, staticClasses } from 'decky-frontend-lib';
import { SteamSpinner } from 'decky-frontend-lib';
import { FC, useEffect, useState } from 'react';
import { LegacyStorePlugin, StorePlugin, getLegacyPluginList, getPluginList } from '../../store';
import PluginCard from './PluginCard';
export interface StorePluginVersion {
name: string;
hash: string;
}
export interface StorePlugin {
id: number;
name: string;
versions: StorePluginVersion[];
author: string;
description: string;
tags: string[];
}
export interface LegacyStorePlugin {
artifact: string;
versions: {
[version: string]: string;
};
author: string;
description: string;
tags: string[];
}
export async function installFromURL(url: string) {
const formData = new FormData();
const splitURL = url.split('/');
formData.append('name', splitURL[splitURL.length - 1].replace('.zip', ''));
formData.append('artifact', url);
await fetch('http://localhost:1337/browser/install_plugin', {
method: 'POST',
body: formData,
});
}
export function requestLegacyPluginInstall(plugin: LegacyStorePlugin, selectedVer: string) {
showModal(
<ModalRoot
onOK={() => {
const formData = new FormData();
formData.append('name', plugin.artifact);
formData.append('artifact', `https://github.com/${plugin.artifact}/archive/refs/tags/${selectedVer}.zip`);
formData.append('version', selectedVer);
formData.append('hash', plugin.versions[selectedVer]);
fetch('http://localhost:1337/browser/install_plugin', {
method: 'POST',
body: formData,
});
}}
onCancel={() => {
// do nothing
}}
>
<div className={staticClasses.Title} style={{ flexDirection: 'column', 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) {
const formData = new FormData();
formData.append('name', plugin.name);
formData.append('artifact', `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${selectedVer.hash}.zip`);
formData.append('version', selectedVer.name);
formData.append('hash', selectedVer.hash);
await fetch('http://localhost:1337/browser/install_plugin', {
method: 'POST',
body: formData,
});
}
const StorePage: FC<{}> = () => {
const [data, setData] = useState<StorePlugin[] | null>(null);
const [legacyData, setLegacyData] = useState<LegacyStorePlugin[] | null>(null);
useEffect(() => {
(async () => {
const res = await fetch('https://beta.deckbrew.xyz/plugins', { method: 'GET' }).then((r) => r.json());
const res = await getPluginList();
console.log(res);
setData(res.filter((x: StorePlugin) => x.name !== 'Example Plugin'));
setData(res);
})();
(async () => {
const res = await fetch('https://plugins.deckbrew.xyz/get_plugins', { method: 'GET' }).then((r) => r.json());
const res = await getLegacyPluginList();
console.log(res);
setLegacyData(res);
})();
+49 -14
View File
@@ -1,3 +1,6 @@
import { ButtonItem, CommonUIModule, webpackCache } from 'decky-frontend-lib';
import { forwardRef } from 'react';
import PluginLoader from './plugin-loader';
import { DeckyUpdater } from './updater';
@@ -8,24 +11,56 @@ declare global {
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) {
if (!window.DeckyPluginLoader.hasPlugin(plugin)) 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.deckyAuthToken = await fetch('http://127.0.0.1:1337/auth/token').then((r) => r.text());
window.deckyHasLoaded = true;
window.DeckyPluginLoader?.dismountAll();
window.DeckyPluginLoader?.deinit();
window.DeckyPluginLoader = new PluginLoader();
window.importDeckyPlugin = function (name: string, version: string) {
window.DeckyPluginLoader?.importPlugin(name, version);
};
window.syncDeckyPlugins = async function () {
const plugins = await (
await fetch('http://127.0.0.1:1337/plugins', {
credentials: 'include',
headers: { Authentication: window.deckyAuthToken },
})
).json();
for (const plugin of plugins) {
if (!window.DeckyPluginLoader.hasPlugin(plugin.name))
window.DeckyPluginLoader?.importPlugin(plugin.name, plugin.version);
}
window.DeckyPluginLoader.checkPluginUpdates();
};
setTimeout(() => window.syncDeckyPlugins(), 5000);
})();
+106 -20
View File
@@ -1,17 +1,20 @@
import { ModalRoot, QuickAccessTab, showModal, staticClasses } from 'decky-frontend-lib';
import { ModalRoot, QuickAccessTab, Router, SteamSpinner, showModal, sleep, staticClasses } from 'decky-frontend-lib';
import { Suspense, lazy } from 'react';
import { FaPlug } from 'react-icons/fa';
import { DeckyState, DeckyStateContextProvider } from './components/DeckyState';
import { DeckyState, DeckyStateContextProvider, useDeckyState } from './components/DeckyState';
import LegacyPlugin from './components/LegacyPlugin';
import PluginInstallModal from './components/modals/PluginInstallModal';
import NotificationBadge from './components/NotificationBadge';
import PluginView from './components/PluginView';
import SettingsPage from './components/settings';
import StorePage from './components/store/Store';
import TitleView from './components/TitleView';
import Logger from './logger';
import { Plugin } from './plugin';
import RouterHook from './router-hook';
import { checkForUpdates } from './store';
import TabsHook from './tabs-hook';
import Toaster from './toaster';
import { VerInfo, callUpdaterMethod } from './updater';
declare global {
interface Window {}
@@ -22,16 +25,22 @@ 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;
// stores a list of plugin names which requested to be reloaded
private pluginReloadQueue: string[] = [];
private pluginReloadQueue: { name: string; version?: string }[] = [];
constructor() {
super(PluginLoader.name);
this.log('Initialized');
const TabIcon = () => {
const { updates, hasLoaderUpdate } = useDeckyState();
return <NotificationBadge show={(updates && updates.size > 0) || hasLoaderUpdate} />;
};
this.tabsHook.add({
id: QuickAccessTab.Decky,
title: null,
@@ -41,19 +50,90 @@ class PluginLoader extends Logger {
<PluginView />
</DeckyStateContextProvider>
),
icon: <FaPlug />,
icon: (
<DeckyStateContextProvider deckyState={this.deckyState}>
<>
<FaPlug />
<TabIcon />
</>
</DeckyStateContextProvider>
),
});
this.routerHook.addRoute('/decky/store', () => <StorePage />);
const StorePage = lazy(() => import('./components/store/Store'));
const SettingsPage = lazy(() => import('./components/settings'));
this.routerHook.addRoute('/decky/store', () => (
<Suspense
fallback={
<div
style={{
marginTop: '40px',
height: 'calc( 100% - 40px )',
overflowY: 'scroll',
}}
>
<SteamSpinner />
</div>
}
>
<StorePage />
</Suspense>
));
this.routerHook.addRoute('/decky/settings', () => {
return (
<DeckyStateContextProvider deckyState={this.deckyState}>
<SettingsPage />
<Suspense
fallback={
<div
style={{
marginTop: '40px',
height: 'calc( 100% - 40px )',
overflowY: 'scroll',
}}
>
<SteamSpinner />
</div>
}
>
<SettingsPage />
</Suspense>
</DeckyStateContextProvider>
);
});
}
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} available!`,
onClick: () => Router.Navigate('/decky/settings'),
});
this.deckyState.setHasLoaderUpdate(true);
}
await sleep(7000);
await this.notifyPluginUpdates();
}
public async checkPluginUpdates() {
const updates = await checkForUpdates(this.plugins);
this.deckyState.setUpdates(updates);
return updates;
}
public async notifyPluginUpdates() {
const updates = await this.checkPluginUpdates();
if (updates?.size > 0) {
this.toaster.toast({
title: 'Decky',
body: `Updates available for ${updates.size} plugin${updates.size > 1 ? 's' : ''}!`,
onClick: () => Router.Navigate('/decky/settings/plugins'),
});
}
}
public addPluginInstallPrompt(artifact: string, version: string, request_id: string, hash: string) {
showModal(
<PluginInstallModal
@@ -70,12 +150,7 @@ class PluginLoader extends Logger {
showModal(
<ModalRoot
onOK={async () => {
const formData = new FormData();
formData.append('name', name);
await fetch('http://localhost:1337/browser/uninstall_plugin', {
method: 'POST',
body: formData,
});
await this.callServerMethod('uninstall_plugin', { name });
}}
onCancel={() => {
// do nothing
@@ -111,10 +186,10 @@ class PluginLoader extends Logger {
this.deckyState.setPlugins(this.plugins);
}
public async importPlugin(name: string) {
public async importPlugin(name: string, version?: string | undefined) {
if (this.reloadLock) {
this.log('Reload currently in progress, adding to queue', name);
this.pluginReloadQueue.push(name);
this.pluginReloadQueue.push({ name, version: version });
return;
}
@@ -127,7 +202,7 @@ class PluginLoader extends Logger {
if (name.startsWith('$LEGACY_')) {
await this.importLegacyPlugin(name.replace('$LEGACY_', ''));
} else {
await this.importReactPlugin(name);
await this.importReactPlugin(name, version);
}
this.deckyState.setPlugins(this.plugins);
@@ -138,18 +213,24 @@ class PluginLoader extends Logger {
this.reloadLock = false;
const nextPlugin = this.pluginReloadQueue.shift();
if (nextPlugin) {
this.importPlugin(nextPlugin);
this.importPlugin(nextPlugin.name, nextPlugin.version);
}
}
}
private async importReactPlugin(name: string) {
let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`);
private async importReactPlugin(name: string, version?: string) {
let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, {
credentials: 'include',
headers: {
Authentication: window.deckyAuthToken,
},
});
if (res.ok) {
let plugin = await eval(await res.text())(this.createPluginAPI(name));
this.plugins.push({
...plugin,
name: name,
version: version,
});
} else throw new Error(`${name} frontend_bundle not OK`);
}
@@ -166,8 +247,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),
});
@@ -178,12 +261,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,
+1
View File
@@ -1,5 +1,6 @@
export interface Plugin {
name: string;
version?: string;
icon: JSX.Element;
content?: JSX.Element;
onDismount?(): void;
+13 -6
View File
@@ -1,6 +1,6 @@
import { afterPatch, findModuleChild, unpatch } from 'decky-frontend-lib';
import { ReactElement, createElement, memo } from 'react';
import type { Route, RouteProps } from 'react-router';
import { ReactElement, ReactNode, cloneElement, createElement, memo } from 'react';
import type { Route } from 'react-router';
import {
DeckyRouterState,
@@ -40,7 +40,7 @@ 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, Route>();
let toReplace = new Map<string, ReactNode>();
const DeckyWrapper = ({ children }: { children: ReactElement }) => {
const { routes, routePatches } = useDeckyRouterState();
@@ -62,17 +62,24 @@ class RouterHook extends Logger {
routeList.forEach((route: Route, index: number) => {
const replaced = toReplace.get(route?.props?.path as string);
if (replaced) {
routeList[index] = 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
createElement(routeList[index].type, routeList[index].props, routeList[index].props.children),
routeList[index].props.children,
);
routePatches.get(route.props.path as string)?.forEach((patch) => {
routeList[index].props = patch(routeList[index].props);
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;
});
}
});
+100
View File
@@ -0,0 +1,100 @@
import { ModalRoot, showModal, staticClasses } from 'decky-frontend-lib';
import { Plugin } from './plugin';
export interface StorePluginVersion {
name: string;
hash: string;
}
export interface StorePlugin {
id: number;
name: string;
versions: StorePluginVersion[];
author: string;
description: string;
tags: string[];
}
export interface LegacyStorePlugin {
artifact: string;
versions: {
[version: string]: string;
};
author: string;
description: string;
tags: string[];
}
// name: version
export type PluginUpdateMapping = Map<string, StorePluginVersion>;
export function getPluginList(): Promise<StorePlugin[]> {
return fetch('https://beta.deckbrew.xyz/plugins', {
method: 'GET',
}).then((r) => r.json());
}
export function getLegacyPluginList(): Promise<LegacyStorePlugin[]> {
return fetch('https://plugins.deckbrew.xyz/get_plugins', {
method: 'GET',
}).then((r) => r.json());
}
export async function installFromURL(url: string) {
const splitURL = url.split('/');
await window.DeckyPluginLoader.callServerMethod('install_plugin', {
name: splitURL[splitURL.length - 1].replace('.zip', ''),
artifact: url,
});
}
export function requestLegacyPluginInstall(plugin: LegacyStorePlugin, selectedVer: string) {
showModal(
<ModalRoot
onOK={() => {
window.DeckyPluginLoader.callServerMethod('install_plugin', {
name: plugin.artifact,
artifact: `https://github.com/${plugin.artifact}/archive/refs/tags/${selectedVer}.zip`,
version: selectedVer,
hash: plugin.versions[selectedVer],
});
}}
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: string, selectedVer: StorePluginVersion) {
await window.DeckyPluginLoader.callServerMethod('install_plugin', {
name: plugin,
artifact: `https://cdn.tzatzikiweeb.moe/file/steam-deck-homebrew/versions/${selectedVer.hash}.zip`,
version: selectedVer.name,
hash: selectedVer.hash,
});
}
export async function checkForUpdates(plugins: Plugin[]): Promise<PluginUpdateMapping> {
const serverData = await getPluginList();
const updateMap = new Map<string, StorePluginVersion>();
for (let plugin of plugins) {
const remotePlugin = serverData?.find((x) => x.name == plugin.name);
if (remotePlugin && remotePlugin.versions?.length > 0 && plugin.version != remotePlugin?.versions?.[0]?.name) {
updateMap.set(plugin.name, remotePlugin.versions[0]);
}
}
return updateMap;
}
export function isLegacyPlugin(plugin: LegacyStorePlugin | StorePlugin): plugin is LegacyStorePlugin {
return 'artifact' in plugin;
}
+1 -1
View File
@@ -103,7 +103,7 @@ 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();
+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;
+21
View File
@@ -11,11 +11,32 @@ export interface DeckyUpdater {
finish: () => void;
}
export interface RemoteVerInfo {
assets: {
browser_download_url: string;
created_at: string;
}[];
name: string;
body: string;
prerelease: boolean;
published_at: string;
tag_name: string;
}
export interface VerInfo {
current: string;
remote: RemoteVerInfo | null;
all: RemoteVerInfo[] | 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),
});
+39
View File
@@ -0,0 +1,39 @@
import { useEffect, useState } from 'react';
interface GetSettingArgs<T> {
key: string;
default: T;
}
interface SetSettingArgs<T> {
key: string;
value: T;
}
export function useSetting<T>(key: string, def: T): [value: T | null, setValue: (value: T) => void] {
const [value, setValue] = useState(def);
const [ready, setReady] = useState<boolean>(false);
useEffect(() => {
(async () => {
const res = (await window.DeckyPluginLoader.callServerMethod('get_setting', {
key,
default: def,
} as GetSettingArgs<T>)) as { result: T };
setReady(true);
setValue(res.result);
})();
}, []);
useEffect(() => {
if (ready)
(async () => {
await window.DeckyPluginLoader.callServerMethod('set_setting', {
key,
value,
} as SetSettingArgs<T>);
})();
}, [value]);
return [value, setValue];
}
-1
View File
@@ -1,6 +1,5 @@
{
"compilerOptions": {
"outDir": "dist",
"module": "ESNext",
"target": "ES2020",
"jsx": "react",