mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-17 08:47:49 +00:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 16a6e9b6a9 | |||
| 6f84cf94b5 | |||
| 7c06db5ece | |||
| aeb2decfc1 | |||
| b7d7ca04e1 | |||
| d4d1c2bbab | |||
| effc4ab0f5 | |||
| 79db0c779d | |||
| fe2b6b0283 | |||
| b9a87cd785 | |||
| 98e9ce881f | |||
| e49bdd9c05 | |||
| d0fd2ac674 | |||
| de1c89af21 | |||
| 8b3f569a09 | |||
| 1930400032 | |||
| 43dee863cd | |||
| 55a7682663 | |||
| d05e8d36b4 | |||
| 0018b8e957 | |||
| 59038f65ac | |||
| 5960c11d60 | |||
| 8d065eab1f | |||
| 3b1b6d28d6 | |||
| 0a735886c9 | |||
| c9430f5be4 | |||
| a4e2237fc0 | |||
| 85d0398e62 | |||
| 30a538e85e | |||
| 84a19203c5 | |||
| 99cda2907d | |||
| a38582d158 | |||
| 9556994e14 | |||
| dee2cfa47b | |||
| 463403be23 | |||
| b68eaca55d | |||
| 114c54c9b0 | |||
| 47e0661773 | |||
| 6c48dfe7f6 | |||
| ed0ae7c9e2 | |||
| ea265ae6df | |||
| 860caf440b | |||
| 64040879f5 | |||
| e92073162a | |||
| 67426af3ef | |||
| 0dbdb4a143 | |||
| c9e9c45b37 | |||
| 6bc8a4fb1d | |||
| 20094c5f75 | |||
| 198591dbd7 | |||
| f21d34506d |
+63
-13
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"]
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Vendored
+8
-8
@@ -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
|
||||
|
||||
+4
-3
@@ -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"))")
|
||||
|
||||
Vendored
+12
-11
@@ -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
|
||||
|
||||
Vendored
+8
-5
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+714
-9
@@ -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
|
||||
|
||||
@@ -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() });
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,9 +7,7 @@ import { useDeckyState } from './DeckyState';
|
||||
const titleStyles: CSSProperties = {
|
||||
display: 'flex',
|
||||
paddingTop: '3px',
|
||||
paddingBottom: '14px',
|
||||
paddingRight: '16px',
|
||||
boxShadow: 'unset',
|
||||
};
|
||||
|
||||
const TitleView: VFC = () => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}}
|
||||
|
||||
@@ -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
@@ -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
@@ -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,5 +1,6 @@
|
||||
export interface Plugin {
|
||||
name: string;
|
||||
version?: string;
|
||||
icon: JSX.Element;
|
||||
content?: JSX.Element;
|
||||
onDismount?(): void;
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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,6 +1,5 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"module": "ESNext",
|
||||
"target": "ES2020",
|
||||
"jsx": "react",
|
||||
|
||||
Reference in New Issue
Block a user