Compare commits

..

27 Commits

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

* Use Environment Variables

* Use method to get USER from a systemd root process

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

* Add separate setters/getters for user vars. Ensure sleep prevents race condition of user setter in while loop
2022-08-08 11:32:14 -07:00
AAGaming 198591dbd7 whoops don't need it here 2022-08-05 21:18:19 -04:00
AAGaming f21d34506d Implement CSRF protection 2022-08-05 21:16:29 -04:00
AAGaming ab6ec98160 API for patching existing routes, lower power use 2022-08-02 18:54:55 -04:00
Beebles f1e809781a forgot one update (#125) 2022-07-30 19:42:16 -07:00
Beebles 789058b72f Readme.md references incorrect github repo (#124) 2022-07-30 18:40:44 -07:00
TrainDoctor 4a68b1430d Update README.md 2022-07-28 13:50:03 -07:00
24 changed files with 589 additions and 171 deletions
+63 -13
View File
@@ -75,12 +75,6 @@ jobs:
name: PluginLoader
path: dist
- name: Bump version and push tag ⏫
id: tag_version
uses: mathieudutour/github-tag-action@v6.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Release 📦
uses: softprops/action-gh-release@v1
with:
@@ -89,9 +83,9 @@ jobs:
files: ./dist/PluginLoader
generate_release_notes: true
nightly:
name: Release the nightly version of the package
if: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.release == 'prerelease') }}
prerelease:
name: Release the pre-release version of the package
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release == 'prerelease' }}
needs: build
runs-on: ubuntu-latest
@@ -105,6 +99,12 @@ jobs:
name: PluginLoader
path: dist
- name: Install semver-tool asdf
uses: asdf-vm/actions/install@v1
with:
tool_versions: |
semver 3.3.0
- name: Get tag 🏷️
id: old_tag
uses: rafarlopes/get-latest-pre-release-tag-action@v1
@@ -112,20 +112,22 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
repository: 'decky-loader'
- name: Prepare tag ⚙️
id: ready_tag
run: |
export VERSION=${{ steps.old_tag.outputs.tag }}
export COMMIT=$(git log -1 --pretty=format:%h)
echo ::set-output name=tag_name::$(sed -r 's/(-.*)?-pre$//' <<< $VERSION)-$COMMIT-pre
echo "VERS: $VERSION"
OUT=$(semver bump prerel "$VERSION")
echo "OUT: $OUT"
echo ::set-output name=tag_name::v$OUT
- name: Push tag 📤
uses: rickstaa/action-create-tag@v1.3.2
if: ${{ steps.ready_tag.outputs.tag_name && github.event_name == 'workflow_dispatch' }}
with:
tag: ${{ steps.ready_tag.outputs.tag_name }}
message: Nightly ${{ steps.ready_tag.outputs.tag_name }}
message: Pre-release ${{ steps.ready_tag.outputs.tag_name }}
- name: Release 📦
uses: softprops/action-gh-release@v1
@@ -137,6 +139,54 @@ jobs:
prerelease: true
generate_release_notes: true
# nightly:
# name: Release the nightly version of the package
# if: ${{ github.event_name == 'schedule' }}
# needs: build
# runs-on: ubuntu-latest
# steps:
# - name: Checkout 🧰
# uses: actions/checkout@v3
# - name: Fetch package artifact ⬇️
# uses: actions/download-artifact@v3
# with:
# name: PluginLoader
# path: dist
# - name: Get tag 🏷️
# id: old_tag
# uses: rafarlopes/get-latest-pre-release-tag-action@v1
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# repository: 'decky-loader'
# - name: Prepare tag ⚙️
# id: ready_tag
# run: |
# export VERSION=${{ steps.old_tag.outputs.tag }}
# export COMMIT=$(git log -1 --pretty=format:%h)
# echo ::set-output name=tag_name::$(sed -r 's/(-.*)?-pre$//' <<< $VERSION)-$COMMIT-nightly
# - name: Push tag 📤
# uses: rickstaa/action-create-tag@v1.3.2
# if: ${{ steps.ready_tag.outputs.tag_name && github.event_name == 'workflow_dispatch' }}
# with:
# tag: ${{ steps.ready_tag.outputs.tag_name }}
# message: Nightly ${{ steps.ready_tag.outputs.tag_name }}
# - name: Release 📦
# uses: softprops/action-gh-release@v1
# if: ${{ github.event_name == 'workflow_dispatch' }}
# with:
# name: Prerelease ${{ steps.ready_tag.outputs.tag_name }}
# tag_name: ${{ steps.ready_tag.outputs.tag_name }}
# files: ./dist/PluginLoader
# prerelease: true
# generate_release_notes: true
# - name: Bump prerelease ⏫
# id: bump
# if: ${{ github.event_name == 'schedule' }}
+7 -7
View File
@@ -11,14 +11,14 @@ Keep an eye on the [Wiki](https://deckbrew.xyz) for more information about Plugi
4. Under Miscellaneous, enable `CEF Remote Debugging`
5. Click on the `STEAM` button and select `Power` -> `Switch to Desktop`
6. Make sure you have a password set with the "passwd" command in terminal to install it ([YouTube Guide](https://www.youtube.com/watch?v=1vOMYGj22rQ)).
7. Open a terminal and paste the following command into it:
- For users:
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/legacy/dist/install_release.sh | sh`
- For the latest pre-release,
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/install_prerelease.sh | sh`
7. Open a terminal and paste the following command into it:
- For the latest pre-release:
- `curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/main/dist/install_prerelease.sh | sh`
- For testers/plugin developers:
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/install_prerelease.sh | sh`
- `curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/main/dist/install_prerelease.sh | sh`
- [Wiki Link](https://deckbrew.xyz/en/loader-dev/development)
- For the legacy version (unsupported):
- `curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/legacy/dist/install_release.sh | sh`
7. Done! Reboot back into Gaming mode and enjoy your plugins!
### Install/Uninstall Plugins
@@ -28,7 +28,7 @@ Keep an eye on the [Wiki](https://deckbrew.xyz) for more information about Plugi
### Uninstall
- Open a terminal and paste the following command into it:
- `curl -L https://github.com/SteamDeckHomebrew/PluginLoader/raw/main/dist/uninstall.sh | sh`
- `curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/main/dist/uninstall.sh | sh`
## Features
- Clean injecting and loading of one or more plugins
+35 -35
View File
@@ -1,20 +1,24 @@
from injector import get_tab
# Full imports
import json
# Partial imports
from aiohttp import ClientSession, web
from asyncio import get_event_loop
from concurrent.futures import ProcessPoolExecutor
from hashlib import sha256
from io import BytesIO
from logging import getLogger
from os import path, rename, listdir
from shutil import rmtree
from aiohttp import ClientSession, web
from io import BytesIO
from zipfile import ZipFile
from concurrent.futures import ProcessPoolExecutor
from asyncio import get_event_loop
from subprocess import call
from time import time
from hashlib import sha256
from subprocess import Popen
from injector import inject_to_tab
from zipfile import ZipFile
import json
# Local modules
from helpers import get_ssl_context, get_user, get_user_group
from injector import get_tab, inject_to_tab
import helpers
logger = getLogger("Browser")
class PluginInstallContext:
def __init__(self, artifact, name, version, hash) -> None:
@@ -25,7 +29,6 @@ class PluginInstallContext:
class PluginBrowser:
def __init__(self, plugin_path, server_instance, plugins) -> None:
self.log = getLogger("browser")
self.plugin_path = plugin_path
self.plugins = plugins
self.install_requests = {}
@@ -41,8 +44,11 @@ class PluginBrowser:
return False
zip_file = ZipFile(zip)
zip_file.extractall(self.plugin_path)
Popen(["chown", "-R", "deck:deck", self.plugin_path])
Popen(["chmod", "-R", "555", self.plugin_path])
code_chown = call(["chown", "-R", get_user()+":"+get_user_group(), self.plugin_path])
code_chmod = call(["chmod", "-R", "555", self.plugin_path])
if code_chown != 0 or code_chmod != 0:
logger.error(f"chown/chmod exited with a non-zero exit code (chown: {code_chown}, chmod: {code_chmod})")
return False
return True
def find_plugin_folder(self, name):
@@ -54,7 +60,7 @@ 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")
@@ -63,15 +69,15 @@ class PluginBrowser:
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))
await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
if self.plugins[name]:
self.plugins[name].stop()
self.plugins.pop(name, None)
rmtree(self.find_plugin_folder(name))
except FileNotFoundError:
self.log.warning(f"Plugin {name} not installed, skipping uninstallation")
logger.warning(f"Plugin {name} not installed, skipping uninstallation")
return web.Response(text="Requested plugin uninstall")
@@ -79,32 +85,26 @@ class PluginBrowser:
try:
await self.uninstall_plugin(name)
except:
self.log.error(f"Plugin {name} not installed, skipping uninstallation")
self.log.info(f"Installing {name} (Version: {version})")
logger.error(f"Plugin {name} not installed, skipping uninstallation")
logger.info(f"Installing {name} (Version: {version})")
async with ClientSession() as client:
self.log.debug(f"Fetching {artifact}")
res = await client.get(artifact, ssl=helpers.get_ssl_context())
logger.debug(f"Fetching {artifact}")
res = await client.get(artifact, ssl=get_ssl_context())
if res.status == 200:
self.log.debug("Got 200. Reading...")
logger.debug("Got 200. Reading...")
data = await res.read()
self.log.debug(f"Read {len(data)} bytes")
logger.debug(f"Read {len(data)} bytes")
res_zip = BytesIO(data)
with ProcessPoolExecutor() as executor:
self.log.debug("Unzipping...")
ret = await get_event_loop().run_in_executor(
executor,
self._unzip_to_plugin_dir,
res_zip,
name,
hash
)
logger.debug("Unzipping...")
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
if ret:
self.log.info(f"Installed {name} (Version: {version})")
logger.info(f"Installed {name} (Version: {version})")
await inject_to_tab("SP", "window.syncDeckyPlugins()")
else:
self.log.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
logger.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
else:
self.log.fatal(f"Could not fetch from URL. {await res.text()}")
logger.fatal(f"Could not fetch from URL. {await res.text()}")
async def install_plugin(self, request):
data = await request.post()
+60 -2
View File
@@ -1,7 +1,65 @@
import ssl
import certifi
import ssl
import uuid
import re
from aiohttp.web import middleware, Response
from subprocess import check_output
from time import sleep
# global vars
csrf_token = str(uuid.uuid4())
ssl_ctx = ssl.create_default_context(cafile=certifi.where())
user = None
group = None
assets_regex = re.compile("^/plugins/.*/assets/.*")
def get_ssl_context():
return ssl_ctx
return ssl_ctx
def get_csrf_token():
return csrf_token
@middleware
async def csrf_middleware(request, handler):
if str(request.method) == "OPTIONS" or request.headers.get('Authentication') == csrf_token or str(request.rel_url) == "/auth/token" or str(request.rel_url).startswith("/plugins/load_main/") or str(request.rel_url).startswith("/static/") or str(request.rel_url).startswith("/legacy/") or str(request.rel_url).startswith("/steam_resource/") or assets_regex.match(str(request.rel_url)):
return await handler(request)
return Response(text='Forbidden', status='403')
# Get the user by checking for the first logged in user. As this is run
# by systemd at startup the process is likely to start before the user
# logs in, so we will wait here until they are available. Note that
# other methods such as getenv wont work as there was no $SUDO_USER to
# start the systemd service.
def set_user():
global user
cmd = "who | awk '{print $1}' | sort | head -1"
while user == None:
name = check_output(cmd, shell=True).decode().strip()
if name not in [None, '']:
user = name
sleep(0.1)
# Get the global user. get_user must be called first.
def get_user() -> str:
global user
if user == None:
raise ValueError("helpers.get_user method called before user variable was set. Run helpers.set_user first.")
return user
# Set the global user group. get_user must be called first
def set_user_group() -> str:
global group
global user
if user == None:
raise ValueError("helpers.set_user_dir method called before user variable was set. Run helpers.set_user first.")
if group == None:
group = check_output(["id", "-g", "-n", user]).decode().strip()
# Get the group of the global user. set_user_group must be called first.
def get_user_group() -> str:
global group
if group == None:
raise ValueError("helpers.get_user_group method called before group variable was set. Run helpers.set_user_group first.")
return group
+7 -1
View File
@@ -8,10 +8,13 @@ window.addEventListener("message", function(evt) {
}, false);
async function call_server_method(method_name, arg_object={}) {
const token = await fetch("http://127.0.0.1:1337/auth/token").then(r => r.text());
const response = await fetch(`http://127.0.0.1:1337/methods/${method_name}`, {
method: 'POST',
credentials: "include",
headers: {
'Content-Type': 'application/json',
Authentication: token
},
body: JSON.stringify(arg_object),
});
@@ -40,10 +43,13 @@ async function fetch_nocors(url, request={}) {
async function call_plugin_method(method_name, arg_object={}) {
if (plugin_name == undefined)
throw new Error("Plugin methods can only be called from inside plugins (duh)");
const token = await fetch("http://127.0.0.1:1337/auth/token").then(r => r.text());
const response = await fetch(`http://127.0.0.1:1337/plugins/${plugin_name}/methods/${method_name}`, {
method: 'POST',
credentials: "include",
headers: {
'Content-Type': 'application/json',
'Content-Type': 'application/json',
Authentication: token
},
body: JSON.stringify({
args: arg_object,
+39 -23
View File
@@ -1,10 +1,35 @@
from logging import DEBUG, INFO, basicConfig, getLogger
from os import getenv
# Full imports
import aiohttp_cors
# Partial imports
from aiohttp import ClientSession
from aiohttp.web import Application, run_app, static, get, Response
from aiohttp_jinja2 import setup as jinja_setup
from asyncio import get_event_loop, sleep
from json import dumps, loads
from logging import DEBUG, INFO, basicConfig, getLogger
from os import getenv, path
from subprocess import call
# local modules
from browser import PluginBrowser
from helpers import csrf_middleware, get_csrf_token, get_user, get_user_group, set_user, set_user_group
from injector import inject_to_tab, tab_has_global_var
from loader import Loader
from updater import Updater
from utilities import Utilities
# Ensure USER and GROUP vars are set first.
# TODO: This isn't the best way to do this but supports the current
# implementation. All the config load and environment setting eventually be
# moved into init or a config/loader method.
set_user()
set_user_group()
USER = get_user()
GROUP = get_user_group()
HOME_PATH = "/home/"+USER
CONFIG = {
"plugin_path": getenv("PLUGIN_PATH", "/home/deck/homebrew/plugins"),
"plugin_path": getenv("PLUGIN_PATH", HOME_PATH+"/homebrew/plugins"),
"chown_plugin_path": getenv("CHOWN_PLUGIN_PATH", "1") == "1",
"server_host": getenv("SERVER_HOST", "127.0.0.1"),
"server_port": int(getenv("SERVER_PORT", "1337")),
@@ -14,25 +39,10 @@ CONFIG = {
basicConfig(level=CONFIG["log_level"], format="[%(module)s][%(levelname)s]: %(message)s")
from asyncio import get_event_loop, sleep
from json import dumps, loads
from os import path
from subprocess import call
import aiohttp_cors
from aiohttp.web import Application, run_app, static
from aiohttp_jinja2 import setup as jinja_setup
from browser import PluginBrowser
from injector import inject_to_tab, tab_has_global_var
from loader import Loader
from utilities import Utilities
from updater import Updater
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,9 +51,10 @@ class PluginManager:
def __init__(self) -> None:
self.loop = get_event_loop()
self.web_app = Application()
self.web_app.middlewares.append(csrf_middleware)
self.cors = aiohttp_cors.setup(self.web_app, defaults={
"https://steamloopback.host": aiohttp_cors.ResourceOptions(expose_headers="*",
allow_headers="*")
allow_headers="*", allow_credentials=True)
})
self.plugin_loader = Loader(self.web_app, CONFIG["plugin_path"], self.loop, CONFIG["live_reload"])
self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.web_app, self.plugin_loader.plugins)
@@ -51,12 +62,13 @@ class PluginManager:
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())
self.loop.set_exception_handler(self.exception_handler)
self.web_app.add_routes([get("/auth/token", self.get_auth_token)])
for route in list(self.web_app.router.routes()):
self.cors.add(route)
self.web_app.add_routes([static("/static", path.join(path.dirname(__file__), 'static'))])
@@ -67,6 +79,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:
@@ -82,9 +97,10 @@ class PluginManager:
#await inject_to_tab("SP", "window.syncDeckyPlugins();")
async def loader_reinjector(self):
await sleep(5)
while True:
await sleep(1)
if not await tab_has_global_var("SP", "DeckyPluginLoader"):
await sleep(5)
if not await tab_has_global_var("SP", "deckyHasLoaded"):
logger.info("Plugin loader isn't present in Steam anymore, reinjecting...")
await self.inject_javascript()
+5 -3
View File
@@ -62,14 +62,16 @@ class Updater:
"updatable": self.localVer != None
}
else:
return {"current": "unknown", "updatable": False}
return {"current": "unknown", "remote": self.remoteVer, "updatable": False}
async def check_for_updates(self):
async with ClientSession() as web:
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", ssl=helpers.get_ssl_context()) as res:
remoteVersions = await res.json()
self.remoteVer = next(filter(lambda ver: ver["prerelease"] and ver["tag_name"].startswith("v") and ver["tag_name"].endswith("-pre"), remoteVersions), None)
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 +81,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"]
+8 -8
View File
@@ -4,12 +4,13 @@
echo "Installing Steam Deck Plugin Loader nightly..."
HOMEBREW_FOLDER=/home/deck/homebrew
USER_DIR="$(getent passwd $SUDO_USER | cut -d: -f6)"
HOMEBREW_FOLDER="${USER_DIR}/homebrew"
# Create folder structure
rm -rf ${HOMEBREW_FOLDER}/services
sudo -u deck mkdir -p ${HOMEBREW_FOLDER}/services
sudo -u deck mkdir -p ${HOMEBREW_FOLDER}/plugins
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services"
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins"
# Download latest nightly build and install it
rm -rf /tmp/plugin_loader
@@ -22,7 +23,7 @@ chmod +x ${HOMEBREW_FOLDER}/services/PluginLoader
systemctl --user stop plugin_loader 2> /dev/null
systemctl --user disable plugin_loader 2> /dev/null
rm -f /home/deck/.config/systemd/user/plugin_loader.service
rm -f ${USER_DIR}/.config/systemd/user/plugin_loader.service
systemctl stop plugin_loader 2> /dev/null
systemctl disable plugin_loader 2> /dev/null
@@ -37,10 +38,9 @@ Type=simple
User=root
Restart=always
ExecStart=/home/deck/homebrew/services/PluginLoader
WorkingDirectory=/home/deck/homebrew/services
Environment=PLUGIN_PATH=/home/deck/homebrew/plugins
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
WorkingDirectory=${HOMEBREW_FOLDER}/services
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
[Install]
WantedBy=multi-user.target
Vendored Executable → Regular
+4 -3
View File
@@ -4,12 +4,13 @@
echo "Installing Steam Deck Plugin Loader pre-release..."
HOMEBREW_FOLDER=/home/deck/homebrew
USER_DIR="$(getent passwd $SUDO_USER | cut -d: -f6)"
HOMEBREW_FOLDER="${USER_DIR}/homebrew"
# # Create folder structure
rm -rf ${HOMEBREW_FOLDER}/services
sudo -u deck mkdir -p ${HOMEBREW_FOLDER}/services
sudo -u deck mkdir -p ${HOMEBREW_FOLDER}/plugins
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services"
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins"
# Download latest release and install it
RELEASE=$(curl -s 'https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases' | jq -r "first(.[] | select(.prerelease == "true"))")
+12 -11
View File
@@ -4,33 +4,34 @@
echo "Installing Steam Deck Plugin Loader release..."
HOMEBREW_FOLDER=/home/deck/homebrew
USER_DIR="$(getent passwd $SUDO_USER | cut -d: -f6)"
HOMEBREW_FOLDER="${USER_DIR}/homebrew"
# Create folder structure
rm -rf ${HOMEBREW_FOLDER}/services
sudo -u deck mkdir -p ${HOMEBREW_FOLDER}/services
sudo -u deck mkdir -p ${HOMEBREW_FOLDER}/plugins
rm -rf "${HOMEBREW_FOLDER}/services"
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services"
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins"
# Download latest release and install it
curl -L https://github.com/SteamDeckHomebrew/PluginLoader/releases/latest/download/PluginLoader --output ${HOMEBREW_FOLDER}/services/PluginLoader
chmod +x ${HOMEBREW_FOLDER}/services/PluginLoader
curl -L https://github.com/SteamDeckHomebrew/PluginLoader/releases/latest/download/PluginLoader --output "${HOMEBREW_FOLDER}/services/PluginLoader"
chmod +x "${HOMEBREW_FOLDER}/services/PluginLoader"
systemctl --user stop plugin_loader 2> /dev/null
systemctl --user disable plugin_loader 2> /dev/null
systemctl stop plugin_loader 2> /dev/null
systemctl disable plugin_loader 2> /dev/null
rm -f /etc/systemd/system/plugin_loader.service
cat > /etc/systemd/system/plugin_loader.service <<- EOM
rm -f "/etc/systemd/system/plugin_loader.service"
cat > "/etc/systemd/system/plugin_loader.service" <<- EOM
[Unit]
Description=SteamDeck Plugin Loader
[Service]
Type=simple
User=root
Restart=always
ExecStart=/home/deck/homebrew/services/PluginLoader
WorkingDirectory=/home/deck/homebrew/services
Environment=PLUGIN_PATH=/home/deck/homebrew/plugins
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
WorkingDirectory=${HOMEBREW_FOLDER}/services
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
[Install]
WantedBy=multi-user.target
EOM
+8 -5
View File
@@ -1,17 +1,20 @@
#!/bin/sh
[ "$UID" -eq 0 ] || exec sudo "$0" "$@"
echo "Uninstalling Steam Deck Plugin Loader..."
HOMEBREW_FOLDER=/home/deck/homebrew
USER_DIR="$(getent passwd $SUDO_USER | cut -d: -f6)"
HOMEBREW_FOLDER="${USER_DIR}/homebrew"
# Disable and remove services
sudo systemctl disable --now plugin_loader.service > /dev/null
sudo rm -f /home/deck/.config/systemd/user/plugin_loader.service
sudo rm -f /etc/systemd/system/plugin_loader.service
sudo rm -f "${USER_DIR}/.config/systemd/user/plugin_loader.service"
sudo rm -f "/etc/systemd/system/plugin_loader.service"
# Remove temporary folder if it exists from the install process
rm -rf /tmp/plugin_loader
rm -rf "/tmp/plugin_loader"
# Cleanup services folder
sudo rm ${HOMEBREW_FOLDER}/services/PluginLoader
sudo rm "${HOMEBREW_FOLDER}/services/PluginLoader"
+1 -1
View File
@@ -37,7 +37,7 @@
}
},
"dependencies": {
"decky-frontend-lib": "^1.2.4",
"decky-frontend-lib": "^1.5.1",
"react-icons": "^4.4.0"
}
}
+10 -4
View File
@@ -9,7 +9,7 @@ specifiers:
'@types/react': 16.14.0
'@types/react-router': 5.1.18
'@types/webpack': ^5.28.0
decky-frontend-lib: ^1.2.4
decky-frontend-lib: ^1.5.1
husky: ^8.0.1
import-sort-style-module: ^6.0.0
inquirer: ^8.2.4
@@ -23,7 +23,7 @@ specifiers:
typescript: ^4.7.4
dependencies:
decky-frontend-lib: 1.2.4
decky-frontend-lib: 1.5.1
react-icons: 4.4.0_react@16.14.0
devDependencies:
@@ -806,8 +806,10 @@ packages:
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.5.1:
resolution: {integrity: sha512-XrcMNxqdXJFyuJYJX4Wmo7DvFVkwBsl8aWU5wfLdPQmcPz3drafyEgqIdO4AxpFuTsHTf1qaHBiYYw349vYpgw==}
dependencies:
minimist: 1.2.6
dev: false
/deepmerge/4.2.2:
@@ -1253,6 +1255,10 @@ packages:
brace-expansion: 1.1.11
dev: true
/minimist/1.2.6:
resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==}
dev: false
/ms/2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: true
+34 -5
View File
@@ -6,17 +6,21 @@ export interface RouterEntry {
component: ComponentType;
}
export type RoutePatch = (route: RouteProps) => RouteProps;
interface PublicDeckyRouterState {
routes: Map<string, RouterEntry>;
routePatches: Map<string, Set<RoutePatch>>;
}
export class DeckyRouterState {
private _routes = new Map<string, RouterEntry>();
private _routePatches = new Map<string, Set<RoutePatch>>();
public eventBus = new EventTarget();
publicState(): PublicDeckyRouterState {
return { routes: this._routes };
return { routes: this._routes, routePatches: this._routePatches };
}
addRoute(path: string, component: RouterEntry['component'], props: RouterEntry['props'] = {}) {
@@ -24,6 +28,26 @@ export class DeckyRouterState {
this.notifyUpdate();
}
addPatch(path: string, patch: RoutePatch) {
let patchList = this._routePatches.get(path);
if (!patchList) {
patchList = new Set();
this._routePatches.set(path, patchList);
}
patchList.add(patch);
this.notifyUpdate();
return patch;
}
removePatch(path: string, patch: RoutePatch) {
const patchList = this._routePatches.get(path);
patchList?.delete(patch);
if (patchList?.size == 0) {
this._routePatches.delete(path);
}
this.notifyUpdate();
}
removeRoute(path: string) {
this._routes.delete(path);
this.notifyUpdate();
@@ -36,6 +60,8 @@ export class DeckyRouterState {
interface DeckyRouterStateContext extends PublicDeckyRouterState {
addRoute(path: string, component: RouterEntry['component'], props: RouterEntry['props']): void;
addPatch(path: string, patch: RoutePatch): RoutePatch;
removePatch(path: string, patch: RoutePatch): void;
removeRoute(path: string): void;
}
@@ -62,12 +88,15 @@ export const DeckyRouterStateContextProvider: FC<Props> = ({ children, deckyRout
return () => deckyRouterState.eventBus.removeEventListener('update', onUpdate);
}, []);
const addRoute = (path: string, component: RouterEntry['component'], props: RouterEntry['props'] = {}) =>
deckyRouterState.addRoute(path, component, props);
const removeRoute = (path: string) => deckyRouterState.removeRoute(path);
const addRoute = deckyRouterState.addRoute.bind(deckyRouterState);
const addPatch = deckyRouterState.addPatch.bind(deckyRouterState);
const removePatch = deckyRouterState.removePatch.bind(deckyRouterState);
const removeRoute = deckyRouterState.removeRoute.bind(deckyRouterState);
return (
<DeckyRouterStateContext.Provider value={{ ...publicDeckyRouterState, addRoute, removeRoute }}>
<DeckyRouterStateContext.Provider
value={{ ...publicDeckyRouterState, addRoute, addPatch, removePatch, removeRoute }}
>
{children}
</DeckyRouterStateContext.Provider>
);
+54
View File
@@ -0,0 +1,54 @@
import { ToastData, findModule, joinClassNames } from 'decky-frontend-lib';
import { FunctionComponent } from 'react';
interface ToastProps {
toast: {
data: ToastData;
nToastDurationMS: number;
};
}
const toastClasses = findModule((mod) => {
if (typeof mod !== 'object') return false;
if (mod.ToastPlaceholder) {
return true;
}
return false;
});
const templateClasses = findModule((mod) => {
if (typeof mod !== 'object') return false;
if (mod.ShortTemplate) {
return true;
}
return false;
});
const Toast: FunctionComponent<ToastProps> = ({ toast }) => {
return (
<div
style={{ '--toast-duration': `${toast.nToastDurationMS}ms` } as React.CSSProperties}
className={joinClassNames(toastClasses.ToastPopup, toastClasses.toastEnter)}
>
<div
onClick={toast.data.onClick}
className={joinClassNames(templateClasses.ShortTemplate, toast.data.className || '')}
>
{toast.data.logo && <div className={templateClasses.StandardLogoDimensions}>{toast.data.logo}</div>}
<div className={joinClassNames(templateClasses.Content, toast.data.contentClassName || '')}>
<div className={templateClasses.Header}>
{toast.data.icon && <div className={templateClasses.Icon}>{toast.data.icon}</div>}
<div className={templateClasses.Title}>{toast.data.title}</div>
</div>
<div className={templateClasses.Body}>{toast.data.body}</div>
</div>
</div>
</div>
);
};
export default Toast;
@@ -2,23 +2,7 @@ import { DialogButton, Field, ProgressBarWithInfo, Spinner } from 'decky-fronten
import { useEffect, useState } from 'react';
import { FaArrowDown } from 'react-icons/fa';
import { callUpdaterMethod, finishUpdate } from '../../../../updater';
interface VerInfo {
current: string;
remote: {
assets: {
browser_download_url: string;
created_at: string;
}[];
name: string;
body: string;
prerelease: boolean;
published_at: string;
tag_name: string;
} | null;
updatable: boolean;
}
import { VerInfo, callUpdaterMethod, finishUpdate } from '../../../../updater';
export default function UpdaterSettings() {
const [versionInfo, setVersionInfo] = useState<VerInfo | null>(null);
+18 -2
View File
@@ -35,6 +35,10 @@ export async function installFromURL(url: string) {
await fetch('http://localhost:1337/browser/install_plugin', {
method: 'POST',
body: formData,
credentials: 'include',
headers: {
Authentication: window.deckyAuthToken,
},
});
}
@@ -50,6 +54,10 @@ export function requestLegacyPluginInstall(plugin: LegacyStorePlugin, selectedVe
fetch('http://localhost:1337/browser/install_plugin', {
method: 'POST',
body: formData,
credentials: 'include',
headers: {
Authentication: window.deckyAuthToken,
},
});
}}
onCancel={() => {
@@ -75,6 +83,10 @@ export async function requestPluginInstall(plugin: StorePlugin, selectedVer: Sto
await fetch('http://localhost:1337/browser/install_plugin', {
method: 'POST',
body: formData,
credentials: 'include',
headers: {
Authentication: window.deckyAuthToken,
},
});
}
@@ -84,12 +96,16 @@ const StorePage: FC<{}> = () => {
useEffect(() => {
(async () => {
const res = await fetch('https://beta.deckbrew.xyz/plugins', { method: 'GET' }).then((r) => r.json());
const res = await fetch('https://beta.deckbrew.xyz/plugins', {
method: 'GET',
}).then((r) => r.json());
console.log(res);
setData(res.filter((x: StorePlugin) => x.name !== 'Example Plugin'));
})();
(async () => {
const res = await fetch('https://plugins.deckbrew.xyz/get_plugins', { method: 'GET' }).then((r) => r.json());
const res = await fetch('https://plugins.deckbrew.xyz/get_plugins', {
method: 'GET',
}).then((r) => r.json());
console.log(res);
setLegacyData(res);
})();
+26 -13
View File
@@ -1,3 +1,5 @@
import { sleep } from 'decky-frontend-lib';
import PluginLoader from './plugin-loader';
import { DeckyUpdater } from './updater';
@@ -7,22 +9,33 @@ declare global {
DeckyUpdater?: DeckyUpdater;
importDeckyPlugin: Function;
syncDeckyPlugins: Function;
deckyHasLoaded: boolean;
deckyAuthToken: string;
}
}
(async () => {
window.deckyHasLoaded = true;
window.deckyAuthToken = await fetch('http://127.0.0.1:1337/auth/token').then((r) => r.text());
window.DeckyPluginLoader?.dismountAll();
window.DeckyPluginLoader?.deinit();
window.DeckyPluginLoader?.dismountAll();
window.DeckyPluginLoader?.deinit();
window.DeckyPluginLoader = new PluginLoader();
window.importDeckyPlugin = function (name: string) {
window.DeckyPluginLoader?.importPlugin(name);
};
window.DeckyPluginLoader = new PluginLoader();
window.importDeckyPlugin = function (name: string) {
window.DeckyPluginLoader?.importPlugin(name);
};
window.syncDeckyPlugins = async function () {
const plugins = await (await fetch('http://127.0.0.1:1337/plugins')).json();
for (const plugin of plugins) {
if (!window.DeckyPluginLoader.hasPlugin(plugin)) window.DeckyPluginLoader?.importPlugin(plugin);
}
};
window.syncDeckyPlugins = async function () {
const plugins = await (
await fetch('http://127.0.0.1:1337/plugins', {
credentials: 'include',
headers: { Authentication: window.deckyAuthToken },
})
).json();
for (const plugin of plugins) {
if (!window.DeckyPluginLoader.hasPlugin(plugin)) window.DeckyPluginLoader?.importPlugin(plugin);
}
};
setTimeout(() => window.syncDeckyPlugins(), 5000);
setTimeout(() => window.syncDeckyPlugins(), 5000);
})();
+11 -1
View File
@@ -8,6 +8,16 @@ export const log = (name: string, ...args: any[]) => {
);
};
export const debug = (name: string, ...args: any[]) => {
console.debug(
`%c Decky %c ${name} %c`,
'background: #16a085; color: black;',
'background: #1abc9c; color: black;',
'color: blue;',
...args,
);
};
export const error = (name: string, ...args: any[]) => {
console.log(
`%c Decky %c ${name} %c`,
@@ -28,7 +38,7 @@ class Logger {
}
debug(...args: any[]) {
log(this.name, ...args);
debug(this.name, ...args);
}
}
+28 -1
View File
@@ -12,6 +12,8 @@ import Logger from './logger';
import { Plugin } from './plugin';
import RouterHook from './router-hook';
import TabsHook from './tabs-hook';
import Toaster from './toaster';
import { VerInfo, callUpdaterMethod } from './updater';
declare global {
interface Window {}
@@ -22,6 +24,7 @@ class PluginLoader extends Logger {
private tabsHook: TabsHook = new TabsHook();
// private windowHook: WindowHook = new WindowHook();
private routerHook: RouterHook = new RouterHook();
private toaster: Toaster = new Toaster();
private deckyState: DeckyState = new DeckyState();
private reloadLock: boolean = false;
@@ -54,6 +57,16 @@ class PluginLoader extends Logger {
});
}
public async notifyUpdates() {
const versionInfo = (await callUpdaterMethod('get_version')).result as VerInfo;
if (versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current) {
this.toaster.toast({
title: 'Decky',
body: `Update to ${versionInfo?.remote?.tag_name} availiable!`,
});
}
}
public addPluginInstallPrompt(artifact: string, version: string, request_id: string, hash: string) {
showModal(
<PluginInstallModal
@@ -75,6 +88,10 @@ class PluginLoader extends Logger {
await fetch('http://localhost:1337/browser/uninstall_plugin', {
method: 'POST',
body: formData,
credentials: 'include',
headers: {
Authentication: window.deckyAuthToken,
},
});
}}
onCancel={() => {
@@ -144,7 +161,12 @@ class PluginLoader extends Logger {
}
private async importReactPlugin(name: string) {
let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`);
let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, {
credentials: 'include',
headers: {
Authentication: window.deckyAuthToken,
},
});
if (res.ok) {
let plugin = await eval(await res.text())(this.createPluginAPI(name));
this.plugins.push({
@@ -166,8 +188,10 @@ class PluginLoader extends Logger {
async callServerMethod(methodName: string, args = {}) {
const response = await fetch(`http://127.0.0.1:1337/methods/${methodName}`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Authentication: window.deckyAuthToken,
},
body: JSON.stringify(args),
});
@@ -178,12 +202,15 @@ class PluginLoader extends Logger {
createPluginAPI(pluginName: string) {
return {
routerHook: this.routerHook,
toaster: this.toaster,
callServerMethod: this.callServerMethod,
async callPluginMethod(methodName: string, args = {}) {
const response = await fetch(`http://127.0.0.1:1337/plugins/${pluginName}/methods/${methodName}`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Authentication: window.deckyAuthToken,
},
body: JSON.stringify({
args,
+44 -13
View File
@@ -1,10 +1,11 @@
import { afterPatch, findModuleChild, unpatch } from 'decky-frontend-lib';
import { ReactElement, createElement, memo } from 'react';
import { ReactElement, ReactNode, cloneElement, createElement, memo } from 'react';
import type { Route } from 'react-router';
import {
DeckyRouterState,
DeckyRouterStateContextProvider,
RoutePatch,
RouterEntry,
useDeckyRouterState,
} from './components/DeckyRouterState';
@@ -38,19 +39,16 @@ class RouterHook extends Logger {
});
let Route: new () => Route;
// Used to store the new replicated routes we create to allow routes to be unpatched.
let toReplace = new Map<string, ReactNode>();
const DeckyWrapper = ({ children }: { children: ReactElement }) => {
const { routes } = useDeckyRouterState();
const { routes, routePatches } = useDeckyRouterState();
let routerIndex = children.props.children[0].props.children.length;
if (
!children.props.children[0].props.children[routerIndex - 1]?.length ||
children.props.children[0].props.children[routerIndex - 1]?.length !== routes.size
) {
if (
children.props.children[0].props.children[routerIndex - 1]?.length &&
children.props.children[0].props.children[routerIndex - 1].length !== routes.size
)
routerIndex--;
const routeList = children.props.children[0].props.children;
let routerIndex = routeList.length;
if (!routeList[routerIndex - 1]?.length || routeList[routerIndex - 1]?.length !== routes.size) {
if (routeList[routerIndex - 1]?.length && routeList[routerIndex - 1].length !== routes.size) routerIndex--;
const newRouterArray: ReactElement[] = [];
routes.forEach(({ component, props }, path) => {
newRouterArray.push(
@@ -59,8 +57,33 @@ class RouterHook extends Logger {
</Route>,
);
});
children.props.children[0].props.children[routerIndex] = newRouterArray;
routeList[routerIndex] = newRouterArray;
}
routeList.forEach((route: Route, index: number) => {
const replaced = toReplace.get(route?.props?.path as string);
if (replaced) {
routeList[index].props.children = replaced;
toReplace.delete(route?.props?.path as string);
}
if (route?.props?.path && routePatches.has(route.props.path as string)) {
toReplace.set(
route?.props?.path as string,
// @ts-ignore
routeList[index].props.children,
);
routePatches.get(route.props.path as string)?.forEach((patch) => {
const oType = routeList[index].props.children.type;
routeList[index].props.children = patch({
...routeList[index].props,
children: {
...cloneElement(routeList[index].props.children),
type: (props) => createElement(oType, props),
},
}).children;
});
}
});
this.debug('Rerendered routes list');
return children;
};
@@ -97,6 +120,14 @@ class RouterHook extends Logger {
this.routerState.addRoute(path, component, props);
}
addPatch(path: string, patch: RoutePatch) {
return this.routerState.addPatch(path, patch);
}
removePatch(path: string, patch: RoutePatch) {
this.routerState.removePatch(path, patch);
}
removeRoute(path: string) {
this.routerState.removeRoute(path);
}
+3 -3
View File
@@ -103,19 +103,19 @@ class TabsHook extends Logger {
}
deinit() {
unpatch(this.cNode.stateNode, 'render');
if (this.cNode) unpatch(this.cNode.stateNode, 'render');
if (this.qAPTree) this.qAPTree.type = this.quickAccess;
if (this.rendererTree) this.rendererTree.type = this.tabRenderer;
if (this.cNode) this.cNode.stateNode.forceUpdate();
}
add(tab: Tab) {
this.log('Adding tab', tab.id, 'to render array');
this.debug('Adding tab', tab.id, 'to render array');
this.tabs.push(tab);
}
removeById(id: number) {
this.log('Removing tab', id);
this.debug('Removing tab', id);
this.tabs = this.tabs.filter((tab) => tab.id !== id);
}
+93
View File
@@ -0,0 +1,93 @@
import { ToastData, afterPatch, findInReactTree, findModuleChild, sleep, unpatch } from 'decky-frontend-lib';
import Toast from './components/Toast';
import Logger from './logger';
declare global {
interface Window {
__TOASTER_INSTANCE: any;
NotificationStore: any;
}
}
class Toaster extends Logger {
private instanceRet: any;
private node: any;
private settingsModule: any;
constructor() {
super('Toaster');
window.__TOASTER_INSTANCE?.deinit?.();
window.__TOASTER_INSTANCE = this;
this.init();
}
async init() {
this.settingsModule = findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (typeof m[prop]?.settings?.bDisableToastsInGame !== 'undefined') return m[prop];
}
});
let instance: any;
while (true) {
instance = findInReactTree(
(document.getElementById('root') as any)._reactRootContainer._internalRoot.current,
(x) => x?.memoizedProps?.className?.startsWith('toastmanager_ToastPlaceholder'),
);
if (instance) break;
this.debug('finding instance');
await sleep(2000);
}
this.node = instance.return.return;
this.node.stateNode.render = (...args: any[]) => {
const ret = this.node.stateNode.__proto__.render.call(this.node.stateNode, ...args);
if (ret) {
this.instanceRet = ret;
afterPatch(ret, 'type', (_: any, ret: any) => {
if (ret?.props?.children[1]?.children?.props?.notification?.decky) {
const toast = ret.props.children[1].children.props.notification;
ret.props.children[1].children.type = () => <Toast toast={toast} />;
}
return ret;
});
}
return ret;
};
this.node.stateNode.forceUpdate();
this.log('Initialized');
}
toast(toast: ToastData) {
const settings = this.settingsModule.settings;
let toastData = {
nNotificationID: window.NotificationStore.m_nNextTestNotificationID++,
rtCreated: Date.now(),
eType: 15,
nToastDurationMS: toast.duration || 5e3,
data: toast,
decky: true,
};
// @ts-ignore
toastData.data.appid = () => 0;
if (
(settings.bDisableAllToasts && !toast.critical) ||
(settings.bDisableToastsInGame && !toast.critical && window.NotificationStore.BIsUserInGame())
)
return;
window.NotificationStore.m_rgNotificationToasts.push(toastData);
window.NotificationStore.DispatchNextToast();
window.NotificationStore.m_rgNotificationToasts.pop();
}
deinit() {
this.instanceRet && unpatch(this.instanceRet, 'type');
this.node && delete this.node.stateNode.render;
this.node && this.node.stateNode.forceUpdate();
}
}
export default Toaster;
+18
View File
@@ -11,11 +11,29 @@ export interface DeckyUpdater {
finish: () => void;
}
export interface VerInfo {
current: string;
remote: {
assets: {
browser_download_url: string;
created_at: string;
}[];
name: string;
body: string;
prerelease: boolean;
published_at: string;
tag_name: string;
} | null;
updatable: boolean;
}
export async function callUpdaterMethod(methodName: string, args = {}) {
const response = await fetch(`http://127.0.0.1:1337/updater/${methodName}`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Authentication: window.deckyAuthToken,
},
body: JSON.stringify(args),
});