mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-17 16:57:50 +00:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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
|
name: PluginLoader
|
||||||
path: dist
|
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 📦
|
- name: Release 📦
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
@@ -89,9 +83,9 @@ jobs:
|
|||||||
files: ./dist/PluginLoader
|
files: ./dist/PluginLoader
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
|
|
||||||
nightly:
|
prerelease:
|
||||||
name: Release the nightly version of the package
|
name: Release the pre-release version of the package
|
||||||
if: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.release == 'prerelease') }}
|
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release == 'prerelease' }}
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
@@ -105,6 +99,12 @@ jobs:
|
|||||||
name: PluginLoader
|
name: PluginLoader
|
||||||
path: dist
|
path: dist
|
||||||
|
|
||||||
|
- name: Install semver-tool asdf
|
||||||
|
uses: asdf-vm/actions/install@v1
|
||||||
|
with:
|
||||||
|
tool_versions: |
|
||||||
|
semver 3.3.0
|
||||||
|
|
||||||
- name: Get tag 🏷️
|
- name: Get tag 🏷️
|
||||||
id: old_tag
|
id: old_tag
|
||||||
uses: rafarlopes/get-latest-pre-release-tag-action@v1
|
uses: rafarlopes/get-latest-pre-release-tag-action@v1
|
||||||
@@ -112,20 +112,22 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
repository: 'decky-loader'
|
repository: 'decky-loader'
|
||||||
|
|
||||||
- name: Prepare tag ⚙️
|
- name: Prepare tag ⚙️
|
||||||
id: ready_tag
|
id: ready_tag
|
||||||
run: |
|
run: |
|
||||||
export VERSION=${{ steps.old_tag.outputs.tag }}
|
export VERSION=${{ steps.old_tag.outputs.tag }}
|
||||||
export COMMIT=$(git log -1 --pretty=format:%h)
|
echo "VERS: $VERSION"
|
||||||
echo ::set-output name=tag_name::$(sed -r 's/(-.*)?-pre$//' <<< $VERSION)-$COMMIT-pre
|
OUT=$(semver bump prerel "$VERSION")
|
||||||
|
echo "OUT: $OUT"
|
||||||
|
echo ::set-output name=tag_name::v$OUT
|
||||||
|
|
||||||
- name: Push tag 📤
|
- name: Push tag 📤
|
||||||
uses: rickstaa/action-create-tag@v1.3.2
|
uses: rickstaa/action-create-tag@v1.3.2
|
||||||
if: ${{ steps.ready_tag.outputs.tag_name && github.event_name == 'workflow_dispatch' }}
|
if: ${{ steps.ready_tag.outputs.tag_name && github.event_name == 'workflow_dispatch' }}
|
||||||
with:
|
with:
|
||||||
tag: ${{ steps.ready_tag.outputs.tag_name }}
|
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 📦
|
- name: Release 📦
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
@@ -137,6 +139,54 @@ jobs:
|
|||||||
prerelease: true
|
prerelease: true
|
||||||
generate_release_notes: 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 ⏫
|
# - name: Bump prerelease ⏫
|
||||||
# id: bump
|
# id: bump
|
||||||
# if: ${{ github.event_name == 'schedule' }}
|
# if: ${{ github.event_name == 'schedule' }}
|
||||||
|
|||||||
+35
-35
@@ -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 logging import getLogger
|
||||||
from os import path, rename, listdir
|
from os import path, rename, listdir
|
||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
from aiohttp import ClientSession, web
|
from subprocess import call
|
||||||
from io import BytesIO
|
|
||||||
from zipfile import ZipFile
|
|
||||||
from concurrent.futures import ProcessPoolExecutor
|
|
||||||
from asyncio import get_event_loop
|
|
||||||
from time import time
|
from time import time
|
||||||
from hashlib import sha256
|
from zipfile import ZipFile
|
||||||
from subprocess import Popen
|
|
||||||
from injector import inject_to_tab
|
|
||||||
|
|
||||||
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:
|
class PluginInstallContext:
|
||||||
def __init__(self, artifact, name, version, hash) -> None:
|
def __init__(self, artifact, name, version, hash) -> None:
|
||||||
@@ -25,7 +29,6 @@ class PluginInstallContext:
|
|||||||
|
|
||||||
class PluginBrowser:
|
class PluginBrowser:
|
||||||
def __init__(self, plugin_path, server_instance, plugins) -> None:
|
def __init__(self, plugin_path, server_instance, plugins) -> None:
|
||||||
self.log = getLogger("browser")
|
|
||||||
self.plugin_path = plugin_path
|
self.plugin_path = plugin_path
|
||||||
self.plugins = plugins
|
self.plugins = plugins
|
||||||
self.install_requests = {}
|
self.install_requests = {}
|
||||||
@@ -41,8 +44,11 @@ class PluginBrowser:
|
|||||||
return False
|
return False
|
||||||
zip_file = ZipFile(zip)
|
zip_file = ZipFile(zip)
|
||||||
zip_file.extractall(self.plugin_path)
|
zip_file.extractall(self.plugin_path)
|
||||||
Popen(["chown", "-R", "deck:deck", self.plugin_path])
|
code_chown = call(["chown", "-R", get_user()+":"+get_user_group(), self.plugin_path])
|
||||||
Popen(["chmod", "-R", "555", 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
|
return True
|
||||||
|
|
||||||
def find_plugin_folder(self, name):
|
def find_plugin_folder(self, name):
|
||||||
@@ -54,7 +60,7 @@ class PluginBrowser:
|
|||||||
if plugin['name'] == name:
|
if plugin['name'] == name:
|
||||||
return path.join(self.plugin_path, folder)
|
return path.join(self.plugin_path, folder)
|
||||||
except:
|
except:
|
||||||
self.log.debug(f"skipping {folder}")
|
logger.debug(f"skipping {folder}")
|
||||||
|
|
||||||
async def uninstall_plugin(self, name):
|
async def uninstall_plugin(self, name):
|
||||||
tab = await get_tab("SP")
|
tab = await get_tab("SP")
|
||||||
@@ -63,15 +69,15 @@ class PluginBrowser:
|
|||||||
if type(name) != str:
|
if type(name) != str:
|
||||||
data = await name.post()
|
data = await name.post()
|
||||||
name = data.get("name", "undefined")
|
name = data.get("name", "undefined")
|
||||||
self.log.info("uninstalling " + name)
|
logger.info("uninstalling " + name)
|
||||||
self.log.info(" at dir " + self.find_plugin_folder(name))
|
logger.info(" at dir " + self.find_plugin_folder(name))
|
||||||
await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
|
await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
|
||||||
if self.plugins[name]:
|
if self.plugins[name]:
|
||||||
self.plugins[name].stop()
|
self.plugins[name].stop()
|
||||||
self.plugins.pop(name, None)
|
self.plugins.pop(name, None)
|
||||||
rmtree(self.find_plugin_folder(name))
|
rmtree(self.find_plugin_folder(name))
|
||||||
except FileNotFoundError:
|
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")
|
return web.Response(text="Requested plugin uninstall")
|
||||||
|
|
||||||
@@ -79,32 +85,26 @@ class PluginBrowser:
|
|||||||
try:
|
try:
|
||||||
await self.uninstall_plugin(name)
|
await self.uninstall_plugin(name)
|
||||||
except:
|
except:
|
||||||
self.log.error(f"Plugin {name} not installed, skipping uninstallation")
|
logger.error(f"Plugin {name} not installed, skipping uninstallation")
|
||||||
self.log.info(f"Installing {name} (Version: {version})")
|
logger.info(f"Installing {name} (Version: {version})")
|
||||||
async with ClientSession() as client:
|
async with ClientSession() as client:
|
||||||
self.log.debug(f"Fetching {artifact}")
|
logger.debug(f"Fetching {artifact}")
|
||||||
res = await client.get(artifact, ssl=helpers.get_ssl_context())
|
res = await client.get(artifact, ssl=get_ssl_context())
|
||||||
if res.status == 200:
|
if res.status == 200:
|
||||||
self.log.debug("Got 200. Reading...")
|
logger.debug("Got 200. Reading...")
|
||||||
data = await res.read()
|
data = await res.read()
|
||||||
self.log.debug(f"Read {len(data)} bytes")
|
logger.debug(f"Read {len(data)} bytes")
|
||||||
res_zip = BytesIO(data)
|
res_zip = BytesIO(data)
|
||||||
with ProcessPoolExecutor() as executor:
|
with ProcessPoolExecutor() as executor:
|
||||||
self.log.debug("Unzipping...")
|
logger.debug("Unzipping...")
|
||||||
ret = await get_event_loop().run_in_executor(
|
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
|
||||||
executor,
|
|
||||||
self._unzip_to_plugin_dir,
|
|
||||||
res_zip,
|
|
||||||
name,
|
|
||||||
hash
|
|
||||||
)
|
|
||||||
if ret:
|
if ret:
|
||||||
self.log.info(f"Installed {name} (Version: {version})")
|
logger.info(f"Installed {name} (Version: {version})")
|
||||||
await inject_to_tab("SP", "window.syncDeckyPlugins()")
|
await inject_to_tab("SP", "window.syncDeckyPlugins()")
|
||||||
else:
|
else:
|
||||||
self.log.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
|
logger.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
|
||||||
else:
|
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):
|
async def install_plugin(self, request):
|
||||||
data = await request.post()
|
data = await request.post()
|
||||||
|
|||||||
+78
-2
@@ -1,7 +1,83 @@
|
|||||||
import ssl
|
|
||||||
import certifi
|
import certifi
|
||||||
|
import ssl
|
||||||
|
import uuid
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from aiohttp.web import middleware, Response
|
||||||
|
from subprocess import check_output
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
|
||||||
|
REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service"
|
||||||
|
|
||||||
|
# global vars
|
||||||
|
csrf_token = str(uuid.uuid4())
|
||||||
ssl_ctx = ssl.create_default_context(cafile=certifi.where())
|
ssl_ctx = ssl.create_default_context(cafile=certifi.where())
|
||||||
|
user = None
|
||||||
|
group = None
|
||||||
|
|
||||||
|
assets_regex = re.compile("^/plugins/.*/assets/.*")
|
||||||
|
|
||||||
def get_ssl_context():
|
def get_ssl_context():
|
||||||
return ssl_ctx
|
return ssl_ctx
|
||||||
|
|
||||||
|
def get_csrf_token():
|
||||||
|
return csrf_token
|
||||||
|
|
||||||
|
@middleware
|
||||||
|
async def csrf_middleware(request, handler):
|
||||||
|
if str(request.method) == "OPTIONS" or request.headers.get('Authentication') == csrf_token or str(request.rel_url) == "/auth/token" or str(request.rel_url).startswith("/plugins/load_main/") or str(request.rel_url).startswith("/static/") or str(request.rel_url).startswith("/legacy/") or str(request.rel_url).startswith("/steam_resource/") or assets_regex.match(str(request.rel_url)):
|
||||||
|
return await handler(request)
|
||||||
|
return Response(text='Forbidden', status='403')
|
||||||
|
|
||||||
|
# Get the user by checking for the first logged in user. As this is run
|
||||||
|
# by systemd at startup the process is likely to start before the user
|
||||||
|
# logs in, so we will wait here until they are available. Note that
|
||||||
|
# other methods such as getenv wont work as there was no $SUDO_USER to
|
||||||
|
# start the systemd service.
|
||||||
|
def set_user():
|
||||||
|
global user
|
||||||
|
cmd = "who | awk '{print $1}' | sort | head -1"
|
||||||
|
while user == None:
|
||||||
|
name = check_output(cmd, shell=True).decode().strip()
|
||||||
|
if name not in [None, '']:
|
||||||
|
user = name
|
||||||
|
sleep(0.1)
|
||||||
|
|
||||||
|
# Get the global user. get_user must be called first.
|
||||||
|
def get_user() -> str:
|
||||||
|
global user
|
||||||
|
if user == None:
|
||||||
|
raise ValueError("helpers.get_user method called before user variable was set. Run helpers.set_user first.")
|
||||||
|
return user
|
||||||
|
|
||||||
|
# Set the global user group. get_user must be called first
|
||||||
|
def set_user_group() -> str:
|
||||||
|
global group
|
||||||
|
global user
|
||||||
|
if user == None:
|
||||||
|
raise ValueError("helpers.set_user_dir method called before user variable was set. Run helpers.set_user first.")
|
||||||
|
if group == None:
|
||||||
|
group = check_output(["id", "-g", "-n", user]).decode().strip()
|
||||||
|
|
||||||
|
# Get the group of the global user. set_user_group must be called first.
|
||||||
|
def get_user_group() -> str:
|
||||||
|
global group
|
||||||
|
if group == None:
|
||||||
|
raise ValueError("helpers.get_user_group method called before group variable was set. Run helpers.set_user_group first.")
|
||||||
|
return group
|
||||||
|
|
||||||
|
async def is_systemd_unit_active(unit_name: str) -> bool:
|
||||||
|
res = subprocess.run(["systemctl", "is-active", unit_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
return res.returncode == 0
|
||||||
|
|
||||||
|
async def stop_systemd_unit(unit_name: str) -> subprocess.CompletedProcess:
|
||||||
|
cmd = ["systemctl", "stop", unit_name]
|
||||||
|
|
||||||
|
return subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
async def start_systemd_unit(unit_name: str) -> subprocess.CompletedProcess:
|
||||||
|
cmd = ["systemctl", "start", unit_name]
|
||||||
|
|
||||||
|
return subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
|||||||
+5
-2
@@ -5,6 +5,7 @@ from logging import debug, getLogger
|
|||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
|
|
||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
|
from aiohttp.client_exceptions import ClientConnectorError
|
||||||
|
|
||||||
BASE_ADDRESS = "http://localhost:8080"
|
BASE_ADDRESS = "http://localhost:8080"
|
||||||
|
|
||||||
@@ -65,11 +66,13 @@ async def get_tabs():
|
|||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
res = await web.get(f"{BASE_ADDRESS}/json")
|
res = await web.get(f"{BASE_ADDRESS}/json")
|
||||||
break
|
except ClientConnectorError:
|
||||||
except:
|
logger.debug("ClientConnectorError excepted.")
|
||||||
logger.debug("Steam isn't available yet. Wait for a moment...")
|
logger.debug("Steam isn't available yet. Wait for a moment...")
|
||||||
logger.debug(format_exc())
|
logger.debug(format_exc())
|
||||||
await sleep(5)
|
await sleep(5)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
if res.status == 200:
|
if res.status == 200:
|
||||||
r = await res.json()
|
r = await res.json()
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ window.addEventListener("message", function(evt) {
|
|||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
async function call_server_method(method_name, arg_object={}) {
|
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}`, {
|
const response = await fetch(`http://127.0.0.1:1337/methods/${method_name}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
Authentication: token
|
||||||
},
|
},
|
||||||
body: JSON.stringify(arg_object),
|
body: JSON.stringify(arg_object),
|
||||||
});
|
});
|
||||||
@@ -40,10 +43,13 @@ async function fetch_nocors(url, request={}) {
|
|||||||
async function call_plugin_method(method_name, arg_object={}) {
|
async function call_plugin_method(method_name, arg_object={}) {
|
||||||
if (plugin_name == undefined)
|
if (plugin_name == undefined)
|
||||||
throw new Error("Plugin methods can only be called from inside plugins (duh)");
|
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}`, {
|
const response = await fetch(`http://127.0.0.1:1337/plugins/${plugin_name}/methods/${method_name}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
Authentication: token
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
args: arg_object,
|
args: arg_object,
|
||||||
|
|||||||
+4
-4
@@ -88,7 +88,7 @@ class Loader:
|
|||||||
|
|
||||||
async def get_plugins(self, request):
|
async def get_plugins(self, request):
|
||||||
plugins = list(self.plugins.values())
|
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_frontend_assets(self, request):
|
||||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||||
@@ -116,13 +116,13 @@ class Loader:
|
|||||||
self.logger.info(f"Plugin {plugin.name} is passive")
|
self.logger.info(f"Plugin {plugin.name} is passive")
|
||||||
self.plugins[plugin.name] = plugin.start()
|
self.plugins[plugin.name] = plugin.start()
|
||||||
self.logger.info(f"Loaded {plugin.name}")
|
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:
|
except Exception as e:
|
||||||
self.logger.error(f"Could not load {file}. {e}")
|
self.logger.error(f"Could not load {file}. {e}")
|
||||||
print_exc()
|
print_exc()
|
||||||
|
|
||||||
async def dispatch_plugin(self, name):
|
async def dispatch_plugin(self, name, version):
|
||||||
await inject_to_tab("SP", f"window.importDeckyPlugin('{name}')")
|
await inject_to_tab("SP", f"window.importDeckyPlugin('{name}', '{version}')")
|
||||||
|
|
||||||
def import_plugins(self):
|
def import_plugins(self):
|
||||||
self.logger.info(f"import plugins from {self.plugin_path}")
|
self.logger.info(f"import plugins from {self.plugin_path}")
|
||||||
|
|||||||
+45
-22
@@ -1,10 +1,36 @@
|
|||||||
from logging import DEBUG, INFO, basicConfig, getLogger
|
# Full imports
|
||||||
from os import getenv
|
import aiohttp_cors
|
||||||
|
|
||||||
|
# Partial imports
|
||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
|
from aiohttp.web import Application, run_app, static, get, Response
|
||||||
|
from aiohttp_jinja2 import setup as jinja_setup
|
||||||
|
from asyncio import get_event_loop, sleep
|
||||||
|
from json import dumps, loads
|
||||||
|
from logging import DEBUG, INFO, basicConfig, getLogger
|
||||||
|
from os import getenv, path
|
||||||
|
from subprocess import call
|
||||||
|
|
||||||
|
# local modules
|
||||||
|
from browser import PluginBrowser
|
||||||
|
from helpers import csrf_middleware, get_csrf_token, get_user, get_user_group, set_user, set_user_group, stop_systemd_unit, REMOTE_DEBUGGER_UNIT
|
||||||
|
from injector import inject_to_tab, tab_has_global_var
|
||||||
|
from loader import Loader
|
||||||
|
from updater import Updater
|
||||||
|
from utilities import Utilities
|
||||||
|
|
||||||
|
# Ensure USER and GROUP vars are set first.
|
||||||
|
# TODO: This isn't the best way to do this but supports the current
|
||||||
|
# implementation. All the config load and environment setting eventually be
|
||||||
|
# moved into init or a config/loader method.
|
||||||
|
set_user()
|
||||||
|
set_user_group()
|
||||||
|
USER = get_user()
|
||||||
|
GROUP = get_user_group()
|
||||||
|
HOME_PATH = "/home/"+USER
|
||||||
|
HOMEBREW_PATH = HOME_PATH+"/homebrew"
|
||||||
CONFIG = {
|
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",
|
"chown_plugin_path": getenv("CHOWN_PLUGIN_PATH", "1") == "1",
|
||||||
"server_host": getenv("SERVER_HOST", "127.0.0.1"),
|
"server_host": getenv("SERVER_HOST", "127.0.0.1"),
|
||||||
"server_port": int(getenv("SERVER_PORT", "1337")),
|
"server_port": int(getenv("SERVER_PORT", "1337")),
|
||||||
@@ -14,36 +40,25 @@ CONFIG = {
|
|||||||
|
|
||||||
basicConfig(level=CONFIG["log_level"], format="[%(module)s][%(levelname)s]: %(message)s")
|
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")
|
logger = getLogger("Main")
|
||||||
|
|
||||||
async def chown_plugin_dir(_):
|
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"]])
|
code_chmod = call(["chmod", "-R", "555", CONFIG["plugin_path"]])
|
||||||
if code_chown != 0 or code_chmod != 0:
|
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})")
|
logger.error(f"chown/chmod exited with a non-zero exit code (chown: {code_chown}, chmod: {code_chmod})")
|
||||||
|
|
||||||
|
def remote_debugging_allowed():
|
||||||
|
return path.exists(HOMEBREW_PATH + "/allow_remote_debugging")
|
||||||
|
|
||||||
class PluginManager:
|
class PluginManager:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.loop = get_event_loop()
|
self.loop = get_event_loop()
|
||||||
self.web_app = Application()
|
self.web_app = Application()
|
||||||
|
self.web_app.middlewares.append(csrf_middleware)
|
||||||
self.cors = aiohttp_cors.setup(self.web_app, defaults={
|
self.cors = aiohttp_cors.setup(self.web_app, defaults={
|
||||||
"https://steamloopback.host": aiohttp_cors.ResourceOptions(expose_headers="*",
|
"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_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.web_app, self.plugin_loader.plugins)
|
||||||
@@ -51,12 +66,15 @@ class PluginManager:
|
|||||||
self.updater = Updater(self)
|
self.updater = Updater(self)
|
||||||
|
|
||||||
jinja_setup(self.web_app)
|
jinja_setup(self.web_app)
|
||||||
self.web_app.on_startup.append(self.inject_javascript)
|
|
||||||
if CONFIG["chown_plugin_path"] == True:
|
if CONFIG["chown_plugin_path"] == True:
|
||||||
self.web_app.on_startup.append(chown_plugin_dir)
|
self.web_app.on_startup.append(chown_plugin_dir)
|
||||||
self.loop.create_task(self.loader_reinjector())
|
self.loop.create_task(self.loader_reinjector())
|
||||||
self.loop.create_task(self.load_plugins())
|
self.loop.create_task(self.load_plugins())
|
||||||
|
if not remote_debugging_allowed():
|
||||||
|
self.loop.create_task(stop_systemd_unit(REMOTE_DEBUGGER_UNIT))
|
||||||
self.loop.set_exception_handler(self.exception_handler)
|
self.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()):
|
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("/static", path.join(path.dirname(__file__), 'static'))])
|
||||||
@@ -67,6 +85,9 @@ class PluginManager:
|
|||||||
return
|
return
|
||||||
loop.default_exception_handler(context)
|
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 def wait_for_server(self):
|
||||||
async with ClientSession() as web:
|
async with ClientSession() as web:
|
||||||
while True:
|
while True:
|
||||||
@@ -82,6 +103,8 @@ class PluginManager:
|
|||||||
#await inject_to_tab("SP", "window.syncDeckyPlugins();")
|
#await inject_to_tab("SP", "window.syncDeckyPlugins();")
|
||||||
|
|
||||||
async def loader_reinjector(self):
|
async def loader_reinjector(self):
|
||||||
|
await sleep(2)
|
||||||
|
await self.inject_javascript()
|
||||||
while True:
|
while True:
|
||||||
await sleep(5)
|
await sleep(5)
|
||||||
if not await tab_has_global_var("SP", "deckyHasLoaded"):
|
if not await tab_has_global_var("SP", "deckyHasLoaded"):
|
||||||
@@ -90,7 +113,7 @@ class PluginManager:
|
|||||||
|
|
||||||
async def inject_javascript(self, request=None):
|
async def inject_javascript(self, request=None):
|
||||||
try:
|
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))};" + open(path.join(path.dirname(__file__), "./static/plugin-loader.iife.js"), "r").read() + "})();}catch(e){console.error(e)}", True)
|
||||||
except:
|
except:
|
||||||
logger.info("Failed to inject JavaScript into tab")
|
logger.info("Failed to inject JavaScript into tab")
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -21,7 +21,13 @@ class PluginWrapper:
|
|||||||
self.socket_addr = f"/tmp/plugin_socket_{time()}"
|
self.socket_addr = f"/tmp/plugin_socket_{time()}"
|
||||||
self.method_call_lock = Lock()
|
self.method_call_lock = Lock()
|
||||||
|
|
||||||
|
self.version = None
|
||||||
|
|
||||||
json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r"))
|
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.legacy = False
|
||||||
self.main_view_html = json["main_view_html"] if "main_view_html" in json else ""
|
self.main_view_html = json["main_view_html"] if "main_view_html" in json else ""
|
||||||
|
|||||||
+5
-3
@@ -62,14 +62,16 @@ class Updater:
|
|||||||
"updatable": self.localVer != None
|
"updatable": self.localVer != None
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return {"current": "unknown", "updatable": False}
|
return {"current": "unknown", "remote": self.remoteVer, "updatable": False}
|
||||||
|
|
||||||
async def check_for_updates(self):
|
async def check_for_updates(self):
|
||||||
async with ClientSession() as web:
|
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:
|
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()
|
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")
|
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()
|
return await self.get_version()
|
||||||
|
|
||||||
async def version_reloader(self):
|
async def version_reloader(self):
|
||||||
@@ -79,7 +81,7 @@ class Updater:
|
|||||||
await self.check_for_updates()
|
await self.check_for_updates()
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
await sleep(60 * 60) # 1 hour
|
await sleep(60 * 60 * 6) # 6 hours
|
||||||
|
|
||||||
async def do_update(self):
|
async def do_update(self):
|
||||||
version = self.remoteVer["tag_name"]
|
version = self.remoteVer["tag_name"]
|
||||||
|
|||||||
+17
-1
@@ -5,6 +5,7 @@ from aiohttp import ClientSession, web
|
|||||||
|
|
||||||
from injector import inject_to_tab
|
from injector import inject_to_tab
|
||||||
import helpers
|
import helpers
|
||||||
|
import subprocess
|
||||||
|
|
||||||
class Utilities:
|
class Utilities:
|
||||||
def __init__(self, context) -> None:
|
def __init__(self, context) -> None:
|
||||||
@@ -16,7 +17,10 @@ class Utilities:
|
|||||||
"confirm_plugin_install": self.confirm_plugin_install,
|
"confirm_plugin_install": self.confirm_plugin_install,
|
||||||
"execute_in_tab": self.execute_in_tab,
|
"execute_in_tab": self.execute_in_tab,
|
||||||
"inject_css_into_tab": self.inject_css_into_tab,
|
"inject_css_into_tab": self.inject_css_into_tab,
|
||||||
"remove_css_from_tab": self.remove_css_from_tab
|
"remove_css_from_tab": self.remove_css_from_tab,
|
||||||
|
"allow_remote_debugging": self.allow_remote_debugging,
|
||||||
|
"disallow_remote_debugging": self.disallow_remote_debugging,
|
||||||
|
"remote_debugging_allowed": self.remote_debugging_allowed
|
||||||
}
|
}
|
||||||
|
|
||||||
if context:
|
if context:
|
||||||
@@ -133,3 +137,15 @@ class Utilities:
|
|||||||
"success": False,
|
"success": False,
|
||||||
"result": e
|
"result": e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def remote_debugging_allowed(self):
|
||||||
|
return await helpers.is_systemd_unit_active(helpers.REMOTE_DEBUGGER_UNIT)
|
||||||
|
|
||||||
|
async def allow_remote_debugging(self):
|
||||||
|
await helpers.start_systemd_unit(helpers.REMOTE_DEBUGGER_UNIT)
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def disallow_remote_debugging(self):
|
||||||
|
await helpers.stop_systemd_unit(helpers.REMOTE_DEBUGGER_UNIT)
|
||||||
|
return True
|
||||||
|
|||||||
Vendored
+8
-8
@@ -4,12 +4,13 @@
|
|||||||
|
|
||||||
echo "Installing Steam Deck Plugin Loader nightly..."
|
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
|
# Create folder structure
|
||||||
rm -rf ${HOMEBREW_FOLDER}/services
|
rm -rf ${HOMEBREW_FOLDER}/services
|
||||||
sudo -u deck mkdir -p ${HOMEBREW_FOLDER}/services
|
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services"
|
||||||
sudo -u deck mkdir -p ${HOMEBREW_FOLDER}/plugins
|
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins"
|
||||||
|
|
||||||
# Download latest nightly build and install it
|
# Download latest nightly build and install it
|
||||||
rm -rf /tmp/plugin_loader
|
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 stop plugin_loader 2> /dev/null
|
||||||
systemctl --user disable 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 stop plugin_loader 2> /dev/null
|
||||||
systemctl disable plugin_loader 2> /dev/null
|
systemctl disable plugin_loader 2> /dev/null
|
||||||
@@ -37,10 +38,9 @@ Type=simple
|
|||||||
User=root
|
User=root
|
||||||
Restart=always
|
Restart=always
|
||||||
|
|
||||||
ExecStart=/home/deck/homebrew/services/PluginLoader
|
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||||
WorkingDirectory=/home/deck/homebrew/services
|
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||||
|
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
|
||||||
Environment=PLUGIN_PATH=/home/deck/homebrew/plugins
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
+4
-3
@@ -4,12 +4,13 @@
|
|||||||
|
|
||||||
echo "Installing Steam Deck Plugin Loader pre-release..."
|
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
|
# # Create folder structure
|
||||||
rm -rf ${HOMEBREW_FOLDER}/services
|
rm -rf ${HOMEBREW_FOLDER}/services
|
||||||
sudo -u deck mkdir -p ${HOMEBREW_FOLDER}/services
|
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services"
|
||||||
sudo -u deck mkdir -p ${HOMEBREW_FOLDER}/plugins
|
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins"
|
||||||
|
|
||||||
# Download latest release and install it
|
# Download latest release and install it
|
||||||
RELEASE=$(curl -s 'https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases' | jq -r "first(.[] | select(.prerelease == "true"))")
|
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..."
|
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
|
# Create folder structure
|
||||||
rm -rf ${HOMEBREW_FOLDER}/services
|
rm -rf "${HOMEBREW_FOLDER}/services"
|
||||||
sudo -u deck mkdir -p ${HOMEBREW_FOLDER}/services
|
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/services"
|
||||||
sudo -u deck mkdir -p ${HOMEBREW_FOLDER}/plugins
|
sudo -u $SUDO_USER mkdir -p "${HOMEBREW_FOLDER}/plugins"
|
||||||
|
|
||||||
# Download latest release and install it
|
# Download latest release and install it
|
||||||
curl -L https://github.com/SteamDeckHomebrew/PluginLoader/releases/latest/download/PluginLoader --output ${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
|
chmod +x "${HOMEBREW_FOLDER}/services/PluginLoader"
|
||||||
|
|
||||||
systemctl --user stop plugin_loader 2> /dev/null
|
systemctl --user stop plugin_loader 2> /dev/null
|
||||||
systemctl --user disable plugin_loader 2> /dev/null
|
systemctl --user disable plugin_loader 2> /dev/null
|
||||||
|
|
||||||
systemctl stop plugin_loader 2> /dev/null
|
systemctl stop plugin_loader 2> /dev/null
|
||||||
systemctl disable plugin_loader 2> /dev/null
|
systemctl disable plugin_loader 2> /dev/null
|
||||||
rm -f /etc/systemd/system/plugin_loader.service
|
rm -f "/etc/systemd/system/plugin_loader.service"
|
||||||
cat > /etc/systemd/system/plugin_loader.service <<- EOM
|
cat > "/etc/systemd/system/plugin_loader.service" <<- EOM
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=SteamDeck Plugin Loader
|
Description=SteamDeck Plugin Loader
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=root
|
User=root
|
||||||
Restart=always
|
Restart=always
|
||||||
ExecStart=/home/deck/homebrew/services/PluginLoader
|
ExecStart=${HOMEBREW_FOLDER}/services/PluginLoader
|
||||||
WorkingDirectory=/home/deck/homebrew/services
|
WorkingDirectory=${HOMEBREW_FOLDER}/services
|
||||||
Environment=PLUGIN_PATH=/home/deck/homebrew/plugins
|
Environment=PLUGIN_PATH=${HOMEBREW_FOLDER}/plugins
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
EOM
|
EOM
|
||||||
|
|||||||
Vendored
+8
-5
@@ -1,17 +1,20 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
|
[ "$UID" -eq 0 ] || exec sudo "$0" "$@"
|
||||||
|
|
||||||
echo "Uninstalling Steam Deck Plugin Loader..."
|
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
|
# Disable and remove services
|
||||||
sudo systemctl disable --now plugin_loader.service > /dev/null
|
sudo systemctl disable --now plugin_loader.service > /dev/null
|
||||||
sudo rm -f /home/deck/.config/systemd/user/plugin_loader.service
|
sudo rm -f "${USER_DIR}/.config/systemd/user/plugin_loader.service"
|
||||||
sudo rm -f /etc/systemd/system/plugin_loader.service
|
sudo rm -f "/etc/systemd/system/plugin_loader.service"
|
||||||
|
|
||||||
# Remove temporary folder if it exists from the install process
|
# Remove temporary folder if it exists from the install process
|
||||||
rm -rf /tmp/plugin_loader
|
rm -rf "/tmp/plugin_loader"
|
||||||
|
|
||||||
# Cleanup services folder
|
# Cleanup services folder
|
||||||
sudo rm ${HOMEBREW_FOLDER}/services/PluginLoader
|
sudo rm "${HOMEBREW_FOLDER}/services/PluginLoader"
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"decky-frontend-lib": "^1.2.4",
|
"decky-frontend-lib": "^1.7.5",
|
||||||
"react-icons": "^4.4.0"
|
"react-icons": "^4.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+6
-4
@@ -9,7 +9,7 @@ specifiers:
|
|||||||
'@types/react': 16.14.0
|
'@types/react': 16.14.0
|
||||||
'@types/react-router': 5.1.18
|
'@types/react-router': 5.1.18
|
||||||
'@types/webpack': ^5.28.0
|
'@types/webpack': ^5.28.0
|
||||||
decky-frontend-lib: ^1.2.4
|
decky-frontend-lib: ^1.7.5
|
||||||
husky: ^8.0.1
|
husky: ^8.0.1
|
||||||
import-sort-style-module: ^6.0.0
|
import-sort-style-module: ^6.0.0
|
||||||
inquirer: ^8.2.4
|
inquirer: ^8.2.4
|
||||||
@@ -23,7 +23,7 @@ specifiers:
|
|||||||
typescript: ^4.7.4
|
typescript: ^4.7.4
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
decky-frontend-lib: 1.2.4
|
decky-frontend-lib: 1.7.5
|
||||||
react-icons: 4.4.0_react@16.14.0
|
react-icons: 4.4.0_react@16.14.0
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
@@ -806,8 +806,10 @@ packages:
|
|||||||
ms: 2.1.2
|
ms: 2.1.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/decky-frontend-lib/1.2.4:
|
/decky-frontend-lib/1.7.5:
|
||||||
resolution: {integrity: sha512-r3mLEey9KUkF68geJVSjNlOz/Fg4vpMKUzoutSceyd8o/J5l+QR+Vf0b3gwK3UN9Sp4Pj4XQ1eB82+/W0ApsFg==}
|
resolution: {integrity: sha512-1OX/Ix9W76gF0NJjfm0k/01LYPmC2k/k+k/qqH8JJPlPHh5+W5P8ZG2T8m5wKsqoP7jx2W3k7RNZBh9vAqFoFw==}
|
||||||
|
dependencies:
|
||||||
|
minimist: 1.2.6
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/deepmerge/4.2.2:
|
/deepmerge/4.2.2:
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ export const DeckyRouterStateContextProvider: FC<Props> = ({ children, deckyRout
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function onUpdate() {
|
function onUpdate() {
|
||||||
console.log('test', deckyRouterState.publicState());
|
|
||||||
setPublicDeckyRouterState({ ...deckyRouterState.publicState() });
|
setPublicDeckyRouterState({ ...deckyRouterState.publicState() });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,30 @@
|
|||||||
import { FC, createContext, useContext, useEffect, useState } from 'react';
|
import { FC, createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Plugin } from '../plugin';
|
import { Plugin } from '../plugin';
|
||||||
|
import { PluginUpdateMapping } from '../store';
|
||||||
|
|
||||||
interface PublicDeckyState {
|
interface PublicDeckyState {
|
||||||
plugins: Plugin[];
|
plugins: Plugin[];
|
||||||
activePlugin: Plugin | null;
|
activePlugin: Plugin | null;
|
||||||
|
updates: PluginUpdateMapping | null;
|
||||||
|
hasLoaderUpdate?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DeckyState {
|
export class DeckyState {
|
||||||
private _plugins: Plugin[] = [];
|
private _plugins: Plugin[] = [];
|
||||||
private _activePlugin: Plugin | null = null;
|
private _activePlugin: Plugin | null = null;
|
||||||
|
private _updates: PluginUpdateMapping | null = null;
|
||||||
|
private _hasLoaderUpdate: boolean = false;
|
||||||
|
|
||||||
public eventBus = new EventTarget();
|
public eventBus = new EventTarget();
|
||||||
|
|
||||||
publicState(): PublicDeckyState {
|
publicState(): PublicDeckyState {
|
||||||
return { plugins: this._plugins, activePlugin: this._activePlugin };
|
return {
|
||||||
|
plugins: this._plugins,
|
||||||
|
activePlugin: this._activePlugin,
|
||||||
|
updates: this._updates,
|
||||||
|
hasLoaderUpdate: this._hasLoaderUpdate,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
setPlugins(plugins: Plugin[]) {
|
setPlugins(plugins: Plugin[]) {
|
||||||
@@ -32,6 +42,16 @@ export class DeckyState {
|
|||||||
this.notifyUpdate();
|
this.notifyUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setUpdates(updates: PluginUpdateMapping) {
|
||||||
|
this._updates = updates;
|
||||||
|
this.notifyUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasLoaderUpdate(hasUpdate: boolean) {
|
||||||
|
this._hasLoaderUpdate = hasUpdate;
|
||||||
|
this.notifyUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
private notifyUpdate() {
|
private notifyUpdate() {
|
||||||
this.eventBus.dispatchEvent(new Event('update'));
|
this.eventBus.dispatchEvent(new Event('update'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { VFC } from 'react';
|
||||||
|
|
||||||
import { useDeckyState } from './DeckyState';
|
import { useDeckyState } from './DeckyState';
|
||||||
|
import NotificationBadge from './NotificationBadge';
|
||||||
|
|
||||||
const PluginView: VFC = () => {
|
const PluginView: VFC = () => {
|
||||||
const { plugins, activePlugin, setActivePlugin } = useDeckyState();
|
const { plugins, updates, activePlugin, setActivePlugin } = useDeckyState();
|
||||||
|
|
||||||
if (activePlugin) {
|
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 (
|
return (
|
||||||
<PanelSection>
|
<div className={joinClassNames(staticClasses.TabGroupPanel, scrollClasses.ScrollPanel, scrollClasses.ScrollY)}>
|
||||||
{plugins
|
<PanelSection>
|
||||||
.filter((p) => p.content)
|
{plugins
|
||||||
.map(({ name, icon }) => (
|
.filter((p) => p.content)
|
||||||
<PanelSectionRow key={name}>
|
.map(({ name, icon }) => (
|
||||||
<ButtonItem layout="below" onClick={() => setActivePlugin(name)}>
|
<PanelSectionRow key={name}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<ButtonItem layout="below" onClick={() => setActivePlugin(name)}>
|
||||||
<div>{icon}</div>
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<div>{name}</div>
|
<div>{icon}</div>
|
||||||
</div>
|
<div>{name}</div>
|
||||||
</ButtonItem>
|
<NotificationBadge show={updates?.has(name)} style={{ top: '-5px', right: '-5px' }} />
|
||||||
</PanelSectionRow>
|
</div>
|
||||||
))}
|
</ButtonItem>
|
||||||
</PanelSection>
|
</PanelSectionRow>
|
||||||
|
))}
|
||||||
|
</PanelSection>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { useDeckyState } from './DeckyState';
|
|||||||
const titleStyles: CSSProperties = {
|
const titleStyles: CSSProperties = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
paddingTop: '3px',
|
paddingTop: '3px',
|
||||||
paddingBottom: '14px',
|
|
||||||
paddingRight: '16px',
|
paddingRight: '16px',
|
||||||
boxShadow: 'unset',
|
boxShadow: 'unset',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -20,9 +20,7 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, ha
|
|||||||
onOK={async () => {
|
onOK={async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await onOK();
|
await onOK();
|
||||||
Router.NavigateBackOrOpenMenu();
|
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 250);
|
||||||
await sleep(250);
|
|
||||||
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 1000);
|
|
||||||
}}
|
}}
|
||||||
onCancel={async () => {
|
onCancel={async () => {
|
||||||
await onCancel();
|
await onCancel();
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Field, Toggle } from 'decky-frontend-lib';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { FaBug } from 'react-icons/fa';
|
||||||
|
|
||||||
|
export default function RemoteDebuggingSettings() {
|
||||||
|
const [allowRemoteDebugging, setAllowRemoteDebugging] = useState<boolean>(false);
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const res = (await window.DeckyPluginLoader.callServerMethod('remote_debugging_allowed')) as { result: boolean };
|
||||||
|
setAllowRemoteDebugging(res.result);
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Field
|
||||||
|
label="Allow Remote CEF Debugging"
|
||||||
|
description={
|
||||||
|
<span style={{ whiteSpace: 'pre-line' }}>
|
||||||
|
Allow unauthenticated access to the CEF debugger to anyone in your network
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
icon={<FaBug style={{ display: 'block' }} />}
|
||||||
|
>
|
||||||
|
<Toggle
|
||||||
|
value={allowRemoteDebugging}
|
||||||
|
onChange={(toggleValue) => {
|
||||||
|
setAllowRemoteDebugging(toggleValue);
|
||||||
|
if (toggleValue) window.DeckyPluginLoader.callServerMethod('allow_remote_debugging');
|
||||||
|
else window.DeckyPluginLoader.callServerMethod('disallow_remote_debugging');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,23 +2,7 @@ import { DialogButton, Field, ProgressBarWithInfo, Spinner } from 'decky-fronten
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { FaArrowDown } from 'react-icons/fa';
|
import { FaArrowDown } from 'react-icons/fa';
|
||||||
|
|
||||||
import { callUpdaterMethod, finishUpdate } from '../../../../updater';
|
import { VerInfo, callUpdaterMethod, finishUpdate } from '../../../../updater';
|
||||||
|
|
||||||
interface VerInfo {
|
|
||||||
current: string;
|
|
||||||
remote: {
|
|
||||||
assets: {
|
|
||||||
browser_download_url: string;
|
|
||||||
created_at: string;
|
|
||||||
}[];
|
|
||||||
name: string;
|
|
||||||
body: string;
|
|
||||||
prerelease: boolean;
|
|
||||||
published_at: string;
|
|
||||||
tag_name: string;
|
|
||||||
} | null;
|
|
||||||
updatable: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function UpdaterSettings() {
|
export default function UpdaterSettings() {
|
||||||
const [versionInfo, setVersionInfo] = useState<VerInfo | null>(null);
|
const [versionInfo, setVersionInfo] = useState<VerInfo | null>(null);
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { DialogButton, Field, TextField } from 'decky-frontend-lib';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { FaShapes } from 'react-icons/fa';
|
import { FaShapes } from 'react-icons/fa';
|
||||||
|
|
||||||
import { installFromURL } from '../../../store/Store';
|
import { installFromURL } from '../../../../store';
|
||||||
|
import RemoteDebuggingSettings from './RemoteDebugging';
|
||||||
import UpdaterSettings from './Updater';
|
import UpdaterSettings from './Updater';
|
||||||
|
|
||||||
export default function GeneralSettings() {
|
export default function GeneralSettings() {
|
||||||
@@ -20,6 +21,7 @@ export default function GeneralSettings() {
|
|||||||
/>
|
/>
|
||||||
</Field> */}
|
</Field> */}
|
||||||
<UpdaterSettings />
|
<UpdaterSettings />
|
||||||
|
<RemoteDebuggingSettings />
|
||||||
<Field
|
<Field
|
||||||
label="Manual plugin install"
|
label="Manual plugin install"
|
||||||
description={<TextField label={'URL'} value={pluginURL} onChange={(e) => setPluginURL(e?.target.value)} />}
|
description={<TextField label={'URL'} value={pluginURL} onChange={(e) => setPluginURL(e?.target.value)} />}
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import { DialogButton, staticClasses } from 'decky-frontend-lib';
|
import { DialogButton, Focusable, Menu, MenuItem, showContextMenu } from 'decky-frontend-lib';
|
||||||
import { FaTrash } from 'react-icons/fa';
|
import { useEffect } from 'react';
|
||||||
|
import { FaDownload, FaEllipsisH } from 'react-icons/fa';
|
||||||
|
|
||||||
|
import { requestPluginInstall } from '../../../../store';
|
||||||
import { useDeckyState } from '../../../DeckyState';
|
import { useDeckyState } from '../../../DeckyState';
|
||||||
|
|
||||||
export default function PluginList() {
|
export default function PluginList() {
|
||||||
const { plugins } = useDeckyState();
|
const { plugins, updates } = useDeckyState();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.DeckyPluginLoader.checkPluginUpdates();
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (plugins.length === 0) {
|
if (plugins.length === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -16,19 +22,45 @@ export default function PluginList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ul style={{ listStyleType: 'none' }}>
|
<ul style={{ listStyleType: 'none' }}>
|
||||||
{plugins.map(({ name }) => (
|
{plugins.map(({ name, version }) => {
|
||||||
<li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
const update = updates?.get(name);
|
||||||
<span>{name}</span>
|
return (
|
||||||
<div className={staticClasses.Title} style={{ marginLeft: 'auto', boxShadow: 'none' }}>
|
<li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', paddingBottom: '10px' }}>
|
||||||
<DialogButton
|
<span>
|
||||||
style={{ height: '40px', width: '40px', padding: '10px 12px' }}
|
{name} {version}
|
||||||
onClick={() => window.DeckyPluginLoader.uninstallPlugin(name)}
|
</span>
|
||||||
>
|
<Focusable style={{ marginLeft: 'auto', boxShadow: 'none', display: 'flex', justifyContent: 'right' }}>
|
||||||
<FaTrash />
|
{update && (
|
||||||
</DialogButton>
|
<DialogButton
|
||||||
</div>
|
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
|
||||||
</li>
|
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>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Router,
|
Router,
|
||||||
SingleDropdownOption,
|
SingleDropdownOption,
|
||||||
SuspensefulImage,
|
SuspensefulImage,
|
||||||
|
joinClassNames,
|
||||||
staticClasses,
|
staticClasses,
|
||||||
} from 'decky-frontend-lib';
|
} from 'decky-frontend-lib';
|
||||||
import { FC, useRef, useState } from 'react';
|
import { FC, useRef, useState } from 'react';
|
||||||
@@ -14,22 +15,15 @@ import {
|
|||||||
LegacyStorePlugin,
|
LegacyStorePlugin,
|
||||||
StorePlugin,
|
StorePlugin,
|
||||||
StorePluginVersion,
|
StorePluginVersion,
|
||||||
|
isLegacyPlugin,
|
||||||
requestLegacyPluginInstall,
|
requestLegacyPluginInstall,
|
||||||
requestPluginInstall,
|
requestPluginInstall,
|
||||||
} from './Store';
|
} from '../../store';
|
||||||
|
|
||||||
interface PluginCardProps {
|
interface PluginCardProps {
|
||||||
plugin: StorePlugin | LegacyStorePlugin;
|
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 PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
||||||
const [selectedOption, setSelectedOption] = useState<number>(0);
|
const [selectedOption, setSelectedOption] = useState<number>(0);
|
||||||
const buttonRef = useRef<HTMLDivElement>(null);
|
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 */}
|
{/* TODO: abstract this messy focus hackiness into a custom component in lib */}
|
||||||
<Focusable
|
<Focusable
|
||||||
// className="Panel Focusable"
|
className="deckyStoreCard"
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
onActivate={(_: CustomEvent) => {
|
onActivate={(_: CustomEvent) => {
|
||||||
buttonRef.current!.focus();
|
buttonRef.current!.focus();
|
||||||
@@ -68,15 +62,17 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
|||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<div className="deckyStoreCardHeader" style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<a
|
<a
|
||||||
style={{ fontSize: '18pt', padding: '10px' }}
|
style={{ fontSize: '18pt', padding: '10px' }}
|
||||||
className={classNames(staticClasses.Text)}
|
className={joinClassNames(staticClasses.Text)}
|
||||||
// onClick={() => Router.NavigateToExternalWeb('https://github.com/' + plugin.artifact)}
|
// onClick={() => Router.NavigateToExternalWeb('https://github.com/' + plugin.artifact)}
|
||||||
>
|
>
|
||||||
{isLegacyPlugin(plugin) ? (
|
{isLegacyPlugin(plugin) ? (
|
||||||
<div>
|
<div className="deckyStoreCardNameContainer">
|
||||||
<span style={{ color: 'grey' }}>{plugin.artifact.split('/')[0]}/</span>
|
<span className="deckyStoreCardLegacyRepoOwner" style={{ color: 'grey' }}>
|
||||||
|
{plugin.artifact.split('/')[0]}/
|
||||||
|
</span>
|
||||||
{plugin.artifact.split('/')[1]}
|
{plugin.artifact.split('/')[1]}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -89,8 +85,10 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
}}
|
}}
|
||||||
|
className="deckyStoreCardBody"
|
||||||
>
|
>
|
||||||
<SuspensefulImage
|
<SuspensefulImage
|
||||||
|
className="deckyStoreCardImage"
|
||||||
suspenseWidth="256px"
|
suspenseWidth="256px"
|
||||||
style={{
|
style={{
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
@@ -113,17 +111,26 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
}}
|
}}
|
||||||
|
className="deckyStoreCardInfo"
|
||||||
>
|
>
|
||||||
<p className={classNames(staticClasses.PanelSectionRow)}>
|
<p className={joinClassNames(staticClasses.PanelSectionRow)}>
|
||||||
<span>Author: {plugin.author}</span>
|
<span>Author: {plugin.author}</span>
|
||||||
</p>
|
</p>
|
||||||
<p className={classNames(staticClasses.PanelSectionRow)}>
|
<p
|
||||||
<span>Tags:</span>
|
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) => (
|
{plugin.tags.map((tag: string) => (
|
||||||
<span
|
<span
|
||||||
|
className="deckyStoreCardTag"
|
||||||
style={{
|
style={{
|
||||||
padding: '5px',
|
padding: '5px',
|
||||||
marginRight: '10px',
|
|
||||||
borderRadius: '5px',
|
borderRadius: '5px',
|
||||||
background: tag == 'root' ? '#842029' : '#ACB2C947',
|
background: tag == 'root' ? '#842029' : '#ACB2C947',
|
||||||
}}
|
}}
|
||||||
@@ -133,10 +140,10 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
|||||||
))}
|
))}
|
||||||
{isLegacyPlugin(plugin) && (
|
{isLegacyPlugin(plugin) && (
|
||||||
<span
|
<span
|
||||||
|
className="deckyStoreCardTag deckyStoreCardLegacyTag"
|
||||||
style={{
|
style={{
|
||||||
color: '#232120',
|
color: '#232120',
|
||||||
padding: '5px',
|
padding: '5px',
|
||||||
marginRight: '10px',
|
|
||||||
borderRadius: '5px',
|
borderRadius: '5px',
|
||||||
background: '#EDE841',
|
background: '#EDE841',
|
||||||
}}
|
}}
|
||||||
@@ -148,6 +155,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
className="deckyStoreCardActionsContainer"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
alignSelf: 'flex-end',
|
alignSelf: 'flex-end',
|
||||||
@@ -156,6 +164,7 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Focusable
|
<Focusable
|
||||||
|
className="deckyStoreCardActions"
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@@ -163,22 +172,25 @@ const PluginCard: FC<PluginCardProps> = ({ plugin }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
className="deckyStoreCardInstallButtonContainer"
|
||||||
style={{
|
style={{
|
||||||
flex: '1',
|
flex: '1',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogButton
|
<DialogButton
|
||||||
|
className="deckyStoreCardInstallButton"
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
isLegacyPlugin(plugin)
|
isLegacyPlugin(plugin)
|
||||||
? requestLegacyPluginInstall(plugin, Object.keys(plugin.versions)[selectedOption])
|
? requestLegacyPluginInstall(plugin, Object.keys(plugin.versions)[selectedOption])
|
||||||
: requestPluginInstall(plugin, plugin.versions[selectedOption])
|
: requestPluginInstall(plugin.name, plugin.versions[selectedOption])
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Install
|
Install
|
||||||
</DialogButton>
|
</DialogButton>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
className="deckyStoreCardVersionDropdownContainer"
|
||||||
style={{
|
style={{
|
||||||
flex: '0.2',
|
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 { FC, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { LegacyStorePlugin, StorePlugin, getLegacyPluginList, getPluginList } from '../../store';
|
||||||
import PluginCard from './PluginCard';
|
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 StorePage: FC<{}> = () => {
|
||||||
const [data, setData] = useState<StorePlugin[] | null>(null);
|
const [data, setData] = useState<StorePlugin[] | null>(null);
|
||||||
const [legacyData, setLegacyData] = useState<LegacyStorePlugin[] | null>(null);
|
const [legacyData, setLegacyData] = useState<LegacyStorePlugin[] | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const res = await fetch('https://beta.deckbrew.xyz/plugins', { method: 'GET' }).then((r) => r.json());
|
const res = await getPluginList();
|
||||||
console.log(res);
|
console.log(res);
|
||||||
setData(res.filter((x: StorePlugin) => x.name !== 'Example Plugin'));
|
setData(res);
|
||||||
})();
|
})();
|
||||||
(async () => {
|
(async () => {
|
||||||
const res = await fetch('https://plugins.deckbrew.xyz/get_plugins', { method: 'GET' }).then((r) => r.json());
|
const res = await getLegacyPluginList();
|
||||||
console.log(res);
|
console.log(res);
|
||||||
setLegacyData(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 PluginLoader from './plugin-loader';
|
||||||
import { DeckyUpdater } from './updater';
|
import { DeckyUpdater } from './updater';
|
||||||
|
|
||||||
@@ -8,24 +11,56 @@ declare global {
|
|||||||
importDeckyPlugin: Function;
|
importDeckyPlugin: Function;
|
||||||
syncDeckyPlugins: Function;
|
syncDeckyPlugins: Function;
|
||||||
deckyHasLoaded: boolean;
|
deckyHasLoaded: boolean;
|
||||||
|
deckyAuthToken: string;
|
||||||
|
webpackJsonp: any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.DeckyPluginLoader?.dismountAll();
|
// HACK to fix plugins using webpack v4 push
|
||||||
window.DeckyPluginLoader?.deinit();
|
|
||||||
|
|
||||||
window.DeckyPluginLoader = new PluginLoader();
|
const v4Cache = {};
|
||||||
window.importDeckyPlugin = function (name: string) {
|
for (let m of Object.keys(webpackCache)) {
|
||||||
window.DeckyPluginLoader?.importPlugin(name);
|
v4Cache[m] = { exports: webpackCache[m] };
|
||||||
};
|
}
|
||||||
|
|
||||||
window.syncDeckyPlugins = async function () {
|
if (!window.webpackJsonp || window.webpackJsonp.deckyShimmed) {
|
||||||
const plugins = await (await fetch('http://127.0.0.1:1337/plugins')).json();
|
window.webpackJsonp = {
|
||||||
for (const plugin of plugins) {
|
deckyShimmed: true,
|
||||||
if (!window.DeckyPluginLoader.hasPlugin(plugin)) window.DeckyPluginLoader?.importPlugin(plugin);
|
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);
|
||||||
|
})();
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { ModalRoot, QuickAccessTab, showModal, staticClasses } from 'decky-frontend-lib';
|
import { ModalRoot, QuickAccessTab, Router, showModal, sleep, staticClasses } from 'decky-frontend-lib';
|
||||||
import { FaPlug } from 'react-icons/fa';
|
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 LegacyPlugin from './components/LegacyPlugin';
|
||||||
import PluginInstallModal from './components/modals/PluginInstallModal';
|
import PluginInstallModal from './components/modals/PluginInstallModal';
|
||||||
|
import NotificationBadge from './components/NotificationBadge';
|
||||||
import PluginView from './components/PluginView';
|
import PluginView from './components/PluginView';
|
||||||
import SettingsPage from './components/settings';
|
import SettingsPage from './components/settings';
|
||||||
import StorePage from './components/store/Store';
|
import StorePage from './components/store/Store';
|
||||||
@@ -11,7 +12,10 @@ import TitleView from './components/TitleView';
|
|||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
import { Plugin } from './plugin';
|
import { Plugin } from './plugin';
|
||||||
import RouterHook from './router-hook';
|
import RouterHook from './router-hook';
|
||||||
|
import { checkForUpdates } from './store';
|
||||||
import TabsHook from './tabs-hook';
|
import TabsHook from './tabs-hook';
|
||||||
|
import Toaster from './toaster';
|
||||||
|
import { VerInfo, callUpdaterMethod } from './updater';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {}
|
interface Window {}
|
||||||
@@ -22,16 +26,22 @@ class PluginLoader extends Logger {
|
|||||||
private tabsHook: TabsHook = new TabsHook();
|
private tabsHook: TabsHook = new TabsHook();
|
||||||
// private windowHook: WindowHook = new WindowHook();
|
// private windowHook: WindowHook = new WindowHook();
|
||||||
private routerHook: RouterHook = new RouterHook();
|
private routerHook: RouterHook = new RouterHook();
|
||||||
|
private toaster: Toaster = new Toaster();
|
||||||
private deckyState: DeckyState = new DeckyState();
|
private deckyState: DeckyState = new DeckyState();
|
||||||
|
|
||||||
private reloadLock: boolean = false;
|
private reloadLock: boolean = false;
|
||||||
// stores a list of plugin names which requested to be reloaded
|
// stores a list of plugin names which requested to be reloaded
|
||||||
private pluginReloadQueue: string[] = [];
|
private pluginReloadQueue: { name: string; version?: string }[] = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(PluginLoader.name);
|
super(PluginLoader.name);
|
||||||
this.log('Initialized');
|
this.log('Initialized');
|
||||||
|
|
||||||
|
const TabIcon = () => {
|
||||||
|
const { updates, hasLoaderUpdate } = useDeckyState();
|
||||||
|
return <NotificationBadge show={(updates && updates.size > 0) || hasLoaderUpdate} />;
|
||||||
|
};
|
||||||
|
|
||||||
this.tabsHook.add({
|
this.tabsHook.add({
|
||||||
id: QuickAccessTab.Decky,
|
id: QuickAccessTab.Decky,
|
||||||
title: null,
|
title: null,
|
||||||
@@ -41,7 +51,14 @@ class PluginLoader extends Logger {
|
|||||||
<PluginView />
|
<PluginView />
|
||||||
</DeckyStateContextProvider>
|
</DeckyStateContextProvider>
|
||||||
),
|
),
|
||||||
icon: <FaPlug />,
|
icon: (
|
||||||
|
<DeckyStateContextProvider deckyState={this.deckyState}>
|
||||||
|
<>
|
||||||
|
<FaPlug />
|
||||||
|
<TabIcon />
|
||||||
|
</>
|
||||||
|
</DeckyStateContextProvider>
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.routerHook.addRoute('/decky/store', () => <StorePage />);
|
this.routerHook.addRoute('/decky/store', () => <StorePage />);
|
||||||
@@ -54,6 +71,37 @@ 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} 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) {
|
public addPluginInstallPrompt(artifact: string, version: string, request_id: string, hash: string) {
|
||||||
showModal(
|
showModal(
|
||||||
<PluginInstallModal
|
<PluginInstallModal
|
||||||
@@ -75,6 +123,10 @@ class PluginLoader extends Logger {
|
|||||||
await fetch('http://localhost:1337/browser/uninstall_plugin', {
|
await fetch('http://localhost:1337/browser/uninstall_plugin', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
Authentication: window.deckyAuthToken,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
@@ -111,10 +163,10 @@ class PluginLoader extends Logger {
|
|||||||
this.deckyState.setPlugins(this.plugins);
|
this.deckyState.setPlugins(this.plugins);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async importPlugin(name: string) {
|
public async importPlugin(name: string, version?: string | undefined) {
|
||||||
if (this.reloadLock) {
|
if (this.reloadLock) {
|
||||||
this.log('Reload currently in progress, adding to queue', name);
|
this.log('Reload currently in progress, adding to queue', name);
|
||||||
this.pluginReloadQueue.push(name);
|
this.pluginReloadQueue.push({ name, version: version });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +179,7 @@ class PluginLoader extends Logger {
|
|||||||
if (name.startsWith('$LEGACY_')) {
|
if (name.startsWith('$LEGACY_')) {
|
||||||
await this.importLegacyPlugin(name.replace('$LEGACY_', ''));
|
await this.importLegacyPlugin(name.replace('$LEGACY_', ''));
|
||||||
} else {
|
} else {
|
||||||
await this.importReactPlugin(name);
|
await this.importReactPlugin(name, version);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.deckyState.setPlugins(this.plugins);
|
this.deckyState.setPlugins(this.plugins);
|
||||||
@@ -138,18 +190,24 @@ class PluginLoader extends Logger {
|
|||||||
this.reloadLock = false;
|
this.reloadLock = false;
|
||||||
const nextPlugin = this.pluginReloadQueue.shift();
|
const nextPlugin = this.pluginReloadQueue.shift();
|
||||||
if (nextPlugin) {
|
if (nextPlugin) {
|
||||||
this.importPlugin(nextPlugin);
|
this.importPlugin(nextPlugin.name, nextPlugin.version);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async importReactPlugin(name: string) {
|
private async importReactPlugin(name: string, version?: 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) {
|
if (res.ok) {
|
||||||
let plugin = await eval(await res.text())(this.createPluginAPI(name));
|
let plugin = await eval(await res.text())(this.createPluginAPI(name));
|
||||||
this.plugins.push({
|
this.plugins.push({
|
||||||
...plugin,
|
...plugin,
|
||||||
name: name,
|
name: name,
|
||||||
|
version: version,
|
||||||
});
|
});
|
||||||
} else throw new Error(`${name} frontend_bundle not OK`);
|
} else throw new Error(`${name} frontend_bundle not OK`);
|
||||||
}
|
}
|
||||||
@@ -166,8 +224,10 @@ class PluginLoader extends Logger {
|
|||||||
async callServerMethod(methodName: string, args = {}) {
|
async callServerMethod(methodName: string, args = {}) {
|
||||||
const response = await fetch(`http://127.0.0.1:1337/methods/${methodName}`, {
|
const response = await fetch(`http://127.0.0.1:1337/methods/${methodName}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
Authentication: window.deckyAuthToken,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(args),
|
body: JSON.stringify(args),
|
||||||
});
|
});
|
||||||
@@ -178,12 +238,15 @@ class PluginLoader extends Logger {
|
|||||||
createPluginAPI(pluginName: string) {
|
createPluginAPI(pluginName: string) {
|
||||||
return {
|
return {
|
||||||
routerHook: this.routerHook,
|
routerHook: this.routerHook,
|
||||||
|
toaster: this.toaster,
|
||||||
callServerMethod: this.callServerMethod,
|
callServerMethod: this.callServerMethod,
|
||||||
async callPluginMethod(methodName: string, args = {}) {
|
async callPluginMethod(methodName: string, args = {}) {
|
||||||
const response = await fetch(`http://127.0.0.1:1337/plugins/${pluginName}/methods/${methodName}`, {
|
const response = await fetch(`http://127.0.0.1:1337/plugins/${pluginName}/methods/${methodName}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
Authentication: window.deckyAuthToken,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
args,
|
args,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export interface Plugin {
|
export interface Plugin {
|
||||||
name: string;
|
name: string;
|
||||||
|
version?: string;
|
||||||
icon: JSX.Element;
|
icon: JSX.Element;
|
||||||
content?: JSX.Element;
|
content?: JSX.Element;
|
||||||
onDismount?(): void;
|
onDismount?(): void;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { afterPatch, findModuleChild, unpatch } from 'decky-frontend-lib';
|
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, RouteProps } from 'react-router';
|
import type { Route } from 'react-router';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DeckyRouterState,
|
DeckyRouterState,
|
||||||
@@ -40,7 +40,7 @@ class RouterHook extends Logger {
|
|||||||
|
|
||||||
let Route: new () => Route;
|
let Route: new () => Route;
|
||||||
// Used to store the new replicated routes we create to allow routes to be unpatched.
|
// 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 DeckyWrapper = ({ children }: { children: ReactElement }) => {
|
||||||
const { routes, routePatches } = useDeckyRouterState();
|
const { routes, routePatches } = useDeckyRouterState();
|
||||||
|
|
||||||
@@ -62,17 +62,24 @@ class RouterHook extends Logger {
|
|||||||
routeList.forEach((route: Route, index: number) => {
|
routeList.forEach((route: Route, index: number) => {
|
||||||
const replaced = toReplace.get(route?.props?.path as string);
|
const replaced = toReplace.get(route?.props?.path as string);
|
||||||
if (replaced) {
|
if (replaced) {
|
||||||
routeList[index] = replaced;
|
routeList[index].props.children = replaced;
|
||||||
toReplace.delete(route?.props?.path as string);
|
toReplace.delete(route?.props?.path as string);
|
||||||
}
|
}
|
||||||
if (route?.props?.path && routePatches.has(route.props.path as string)) {
|
if (route?.props?.path && routePatches.has(route.props.path as string)) {
|
||||||
toReplace.set(
|
toReplace.set(
|
||||||
route?.props?.path as string,
|
route?.props?.path as string,
|
||||||
// @ts-ignore
|
// @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) => {
|
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,121 @@
|
|||||||
|
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 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,
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
Authentication: window.deckyAuthToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requestLegacyPluginInstall(plugin: LegacyStorePlugin, selectedVer: string) {
|
||||||
|
showModal(
|
||||||
|
<ModalRoot
|
||||||
|
onOK={() => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('name', plugin.artifact);
|
||||||
|
formData.append('artifact', `https://github.com/${plugin.artifact}/archive/refs/tags/${selectedVer}.zip`);
|
||||||
|
formData.append('version', selectedVer);
|
||||||
|
formData.append('hash', plugin.versions[selectedVer]);
|
||||||
|
fetch('http://localhost:1337/browser/install_plugin', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
Authentication: window.deckyAuthToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
// do nothing
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={staticClasses.Title} style={{ flexDirection: 'column', boxShadow: 'unset' }}>
|
||||||
|
Using legacy plugins
|
||||||
|
</div>
|
||||||
|
You are currently installing a <b>legacy</b> plugin. Legacy plugins are no longer supported and may have issues.
|
||||||
|
Legacy plugins do not support gamepad input. To interact with a legacy plugin, you will need to use the
|
||||||
|
touchscreen.
|
||||||
|
</ModalRoot>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requestPluginInstall(plugin: string, selectedVer: StorePluginVersion) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('name', plugin);
|
||||||
|
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,
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
Authentication: window.deckyAuthToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
deinit() {
|
||||||
unpatch(this.cNode.stateNode, 'render');
|
if (this.cNode) unpatch(this.cNode.stateNode, 'render');
|
||||||
if (this.qAPTree) this.qAPTree.type = this.quickAccess;
|
if (this.qAPTree) this.qAPTree.type = this.quickAccess;
|
||||||
if (this.rendererTree) this.rendererTree.type = this.tabRenderer;
|
if (this.rendererTree) this.rendererTree.type = this.tabRenderer;
|
||||||
if (this.cNode) this.cNode.stateNode.forceUpdate();
|
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,29 @@ export interface DeckyUpdater {
|
|||||||
finish: () => void;
|
finish: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface VerInfo {
|
||||||
|
current: string;
|
||||||
|
remote: {
|
||||||
|
assets: {
|
||||||
|
browser_download_url: string;
|
||||||
|
created_at: string;
|
||||||
|
}[];
|
||||||
|
name: string;
|
||||||
|
body: string;
|
||||||
|
prerelease: boolean;
|
||||||
|
published_at: string;
|
||||||
|
tag_name: string;
|
||||||
|
} | null;
|
||||||
|
updatable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export async function callUpdaterMethod(methodName: string, args = {}) {
|
export async function callUpdaterMethod(methodName: string, args = {}) {
|
||||||
const response = await fetch(`http://127.0.0.1:1337/updater/${methodName}`, {
|
const response = await fetch(`http://127.0.0.1:1337/updater/${methodName}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
Authentication: window.deckyAuthToken,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(args),
|
body: JSON.stringify(args),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user