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