mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-29 14:39:14 +00:00
Compare commits
36 Commits
v2.1.0-pre1
...
v2.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 2461f52ca7 | |||
| 3c00eb8cf4 | |||
| 21e1d8504a | |||
| ba93c4add2 | |||
| 61fea41c8a | |||
| e40d3e4db5 | |||
| bbad6bf2be | |||
| 4e04455163 | |||
| 314292b042 | |||
| a264f36966 | |||
| 60c8c5db42 | |||
| 852c52c59a | |||
| 3136ad72ed | |||
| 3700dd7437 | |||
| c6d48389c9 | |||
| 490fc18008 | |||
| 797c7ea3b0 | |||
| 0f06bc1ef0 | |||
| c774451ff4 | |||
| 62a5bdbbb0 | |||
| 7716c73014 | |||
| 8829adc5b6 | |||
| 62bd3e76bd | |||
| 9867d7bea0 | |||
| c4d6731401 | |||
| fded2fa8bf | |||
| 90c523ec45 | |||
| c5ccb4dfb8 | |||
| 8b1925bc53 | |||
| a8c7c2f18f | |||
| 463258febb | |||
| 304fc0f94c | |||
| b5b041fdee | |||
| 9d980618a7 | |||
| 6dad3f81e8 | |||
| adc1a792fb |
@@ -93,7 +93,7 @@ jobs:
|
||||
uses: rez0n/actions-github-release@main
|
||||
id: latest_release
|
||||
env:
|
||||
# token: ${{ secrets.GITHUB_TOKEN }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: "SteamDeckHomebrew/decky-loader"
|
||||
type: "nodraft"
|
||||
|
||||
@@ -159,14 +159,14 @@ jobs:
|
||||
uses: rez0n/actions-github-release@main
|
||||
id: latest_release
|
||||
env:
|
||||
# token: ${{ secrets.GITHUB_TOKEN }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
repository: "SteamDeckHomebrew/decky-loader"
|
||||
type: "nodraft"
|
||||
|
||||
- name: Prepare tag ⚙️
|
||||
id: ready_tag
|
||||
run: |
|
||||
export VERSION="2.0.5"
|
||||
export VERSION=${{ steps.latest_release.outputs.release }}
|
||||
echo "VERS: $VERSION"
|
||||
OUT=""
|
||||
if [[ ! "$VERSION" =~ "-pre" ]]; then
|
||||
|
||||
Vendored
+21
-15
@@ -89,17 +89,6 @@
|
||||
"command": "rsync -azp --delete --rsh='ssh -p ${config:deckport} ${config:deckkey}' --exclude='.git/' --exclude='.github/' --exclude='.vscode/' --exclude='frontend/' --exclude='dist/' --exclude='contrib/' --exclude='*.log' --exclude='requirements.txt' --exclude='backend/__pycache__/' --exclude='.gitignore' . deck@${config:deckip}:${config:deckdir}/homebrew/dev/pluginloader",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "deployall",
|
||||
"dependsOrder": "sequence",
|
||||
"group": "none",
|
||||
"dependsOn": [
|
||||
"createfolders",
|
||||
"dependencies",
|
||||
"deploy"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
// RUN
|
||||
{
|
||||
"label": "runpydeck",
|
||||
@@ -107,7 +96,7 @@
|
||||
"type": "shell",
|
||||
"group": "none",
|
||||
"dependsOn" : ["checkforsettings"],
|
||||
"command": "ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'export PLUGIN_PATH=${config:deckdir}/homebrew/dev/plugins; export CHOWN_PLUGIN_PATH=0; echo '${config:deckpass}' | sudo -SE python3 ${config:deckdir}/homebrew/dev/pluginloader/backend/main.py'",
|
||||
"command": "ssh deck@${config:deckip} -p ${config:deckport} ${config:deckkey} 'export PLUGIN_PATH=${config:deckdir}/homebrew/dev/plugins; export CHOWN_PLUGIN_PATH=0; export LOG_LEVEL=DEBUG; cd ${config:deckdir}/homebrew/services; echo '${config:deckpass}' | sudo -SE python3 ${config:deckdir}/homebrew/dev/pluginloader/backend/main.py'",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
@@ -126,12 +115,12 @@
|
||||
"group": "none",
|
||||
"dependsOn": [
|
||||
"buildall",
|
||||
"deployall",
|
||||
"deploy",
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "allinone",
|
||||
"label": "updateandrun",
|
||||
"detail": "Build, deploy and run",
|
||||
"dependsOrder": "sequence",
|
||||
"group": {
|
||||
@@ -140,7 +129,24 @@
|
||||
},
|
||||
"dependsOn": [
|
||||
"buildall",
|
||||
"deployall",
|
||||
"deploy",
|
||||
"runpydeck"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "allinone",
|
||||
"detail": "Build, install dependencies, deploy and run",
|
||||
"dependsOrder": "sequence",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": false
|
||||
},
|
||||
"dependsOn": [
|
||||
"buildall",
|
||||
"createfolders",
|
||||
"dependencies",
|
||||
"deploy",
|
||||
"runpydeck"
|
||||
],
|
||||
"problemMatcher": []
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
# Plugin Loader [](https://discord.gg/ZU74G2NJzk)
|
||||
# Decky Loader [](https://discord.gg/ZU74G2NJzk)
|
||||
|
||||

|
||||

|
||||
|
||||
Keep an eye on the [Wiki](https://deckbrew.xyz) for more information about Plugin Loader, documentation + tools for plugin development and more.
|
||||
Keep an eye on the [Wiki](https://deckbrew.xyz) for more information about Decky Loader, documentation + tools for plugin development and more.
|
||||
|
||||
## Installation
|
||||
1. Go into the Steam Deck Settings
|
||||
2. Under System -> System Settings toggle `Enable Developer Mode`
|
||||
3. Scroll the sidebar all the way down and click on `Developer`
|
||||
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:
|
||||
5. Confirm dialog and wait for system reboot
|
||||
6. Click on the `STEAM` button and select `Power` -> `Switch to Desktop`
|
||||
7. 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)).
|
||||
8. Open a terminal ("Konsole" is the pre-installed terminal application) and paste the following command into it:
|
||||
- For the latest release:
|
||||
- `curl -L https://github.com/SteamDeckHomebrew/decky-loader/raw/main/dist/install_prerelease.sh | sh`
|
||||
- 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/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!
|
||||
9. Done! Reboot back into Gaming mode and enjoy your plugins!
|
||||
|
||||
### Install/Uninstall Plugins
|
||||
- Using the shopping bag button in the top right corner, you can go to the offical ["Plugin Store"](https://plugins.deckbrew.xyz/)
|
||||
- Using the shopping bag button in the top right corner of the plugin menu, you can go to the offical Plugin Store ([Web Preview](https://beta.deckbrew.xyz/)).
|
||||
- Simply copy the plugin's folder into `~/homebrew/plugins`
|
||||
- Use the settings menu to uninstall plugins, this will not remove any files made in different directories by plugins.
|
||||
|
||||
@@ -41,8 +42,8 @@ Keep an eye on the [Wiki](https://deckbrew.xyz) for more information about Plugi
|
||||
- There is no complete plugin development documentation yet. However a good starting point is the [Plugin Template](https://github.com/SteamDeckHomebrew/decky-plugin-template) repository.
|
||||
|
||||
## [Contribution](https://deckbrew.xyz/en/loader-dev/development)
|
||||
- Please consult the [Wiki](https://deckbrew.xyz/en/loader-dev/development) for installing development versions of PluginLoader.
|
||||
- This is also useful for Plugin Developers looking to target new but unreleased versions of PluginLoader.
|
||||
- Please consult the [Wiki](https://deckbrew.xyz/en/loader-dev/development) for installing development versions of Decky Loader.
|
||||
- This is also useful for Plugin Developers looking to target new but unreleased versions of Decky Loader.
|
||||
- [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.)
|
||||
|
||||
|
||||
+70
-8
@@ -3,19 +3,19 @@ import json
|
||||
|
||||
# Partial imports
|
||||
from aiohttp import ClientSession, web
|
||||
from asyncio import get_event_loop
|
||||
from asyncio import get_event_loop, sleep
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
from hashlib import sha256
|
||||
from io import BytesIO
|
||||
from logging import getLogger
|
||||
from os import path, rename, listdir
|
||||
from os import R_OK, W_OK, path, rename, listdir, access, mkdir
|
||||
from shutil import rmtree
|
||||
from subprocess import call
|
||||
from time import time
|
||||
from zipfile import ZipFile
|
||||
|
||||
# Local modules
|
||||
from helpers import get_ssl_context, get_user, get_user_group
|
||||
from helpers import get_ssl_context, get_user, get_user_group, download_remote_binary_to_path
|
||||
from injector import get_tab, inject_to_tab
|
||||
|
||||
logger = getLogger("Browser")
|
||||
@@ -28,9 +28,10 @@ class PluginInstallContext:
|
||||
self.hash = hash
|
||||
|
||||
class PluginBrowser:
|
||||
def __init__(self, plugin_path, plugins) -> None:
|
||||
def __init__(self, plugin_path, plugins, loader) -> None:
|
||||
self.plugin_path = plugin_path
|
||||
self.plugins = plugins
|
||||
self.loader = loader
|
||||
self.install_requests = {}
|
||||
|
||||
def _unzip_to_plugin_dir(self, zip, name, hash):
|
||||
@@ -39,12 +40,55 @@ class PluginBrowser:
|
||||
return False
|
||||
zip_file = ZipFile(zip)
|
||||
zip_file.extractall(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])
|
||||
plugin_dir = self.find_plugin_folder(name)
|
||||
code_chown = call(["chown", "-R", get_user()+":"+get_user_group(), plugin_dir])
|
||||
code_chmod = call(["chmod", "-R", "555", plugin_dir])
|
||||
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
|
||||
|
||||
async def _download_remote_binaries_for_plugin_with_name(self, pluginBasePath):
|
||||
rv = False
|
||||
try:
|
||||
packageJsonPath = path.join(pluginBasePath, 'package.json')
|
||||
pluginBinPath = path.join(pluginBasePath, 'bin')
|
||||
|
||||
if access(packageJsonPath, R_OK):
|
||||
with open(packageJsonPath, 'r') as f:
|
||||
packageJson = json.load(f)
|
||||
if len(packageJson["remote_binary"]) > 0:
|
||||
# create bin directory if needed.
|
||||
rc=call(["chmod", "-R", "777", pluginBasePath])
|
||||
if access(pluginBasePath, W_OK):
|
||||
|
||||
if not path.exists(pluginBinPath):
|
||||
mkdir(pluginBinPath)
|
||||
|
||||
if not access(pluginBinPath, W_OK):
|
||||
rc=call(["chmod", "-R", "777", pluginBinPath])
|
||||
|
||||
rv = True
|
||||
for remoteBinary in packageJson["remote_binary"]:
|
||||
# Required Fields. If any Remote Binary is missing these fail the install.
|
||||
binName = remoteBinary["name"]
|
||||
binURL = remoteBinary["url"]
|
||||
binHash = remoteBinary["sha256hash"]
|
||||
if not await download_remote_binary_to_path(binURL, binHash, path.join(pluginBinPath, binName)):
|
||||
rv = False
|
||||
raise Exception(f"Error Downloading Remote Binary {binName}@{binURL} with hash {binHash} to {path.join(pluginBinPath, binName)}")
|
||||
|
||||
code_chown = call(["chown", "-R", get_user()+":"+get_user_group(), self.plugin_path])
|
||||
rc=call(["chmod", "-R", "555", pluginBasePath])
|
||||
else:
|
||||
rv = True
|
||||
logger.debug(f"No Remote Binaries to Download")
|
||||
|
||||
except Exception as e:
|
||||
rv = False
|
||||
logger.debug(str(e))
|
||||
|
||||
return rv
|
||||
|
||||
def find_plugin_folder(self, name):
|
||||
for folder in listdir(self.plugin_path):
|
||||
@@ -58,6 +102,8 @@ class PluginBrowser:
|
||||
logger.debug(f"skipping {folder}")
|
||||
|
||||
async def uninstall_plugin(self, name):
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = True
|
||||
tab = await get_tab("SP")
|
||||
try:
|
||||
logger.info("uninstalling " + name)
|
||||
@@ -74,8 +120,12 @@ class PluginBrowser:
|
||||
except Exception as e:
|
||||
logger.error(f"Plugin {name} in {self.find_plugin_folder(name)} was not uninstalled")
|
||||
logger.error(f"Error at %s", exc_info=e)
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = False
|
||||
|
||||
async def _install(self, artifact, name, version, hash):
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = True
|
||||
try:
|
||||
await self.uninstall_plugin(name)
|
||||
except:
|
||||
@@ -92,10 +142,22 @@ class PluginBrowser:
|
||||
logger.debug("Unzipping...")
|
||||
ret = self._unzip_to_plugin_dir(res_zip, name, hash)
|
||||
if ret:
|
||||
logger.info(f"Installed {name} (Version: {version})")
|
||||
await inject_to_tab("SP", "window.syncDeckyPlugins()")
|
||||
plugin_dir = self.find_plugin_folder(name)
|
||||
ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir)
|
||||
if ret:
|
||||
logger.info(f"Installed {name} (Version: {version})")
|
||||
if name in self.loader.plugins:
|
||||
self.loader.plugins[name].stop()
|
||||
self.loader.plugins.pop(name, None)
|
||||
await sleep(1)
|
||||
self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_dir)
|
||||
# await inject_to_tab("SP", "window.syncDeckyPlugins()")
|
||||
else:
|
||||
logger.fatal(f"Failed Downloading Remote Binaries")
|
||||
else:
|
||||
self.log.fatal(f"SHA-256 Mismatch!!!! {name} (Version: {version})")
|
||||
if self.loader.watcher:
|
||||
self.loader.watcher.disabled = False
|
||||
else:
|
||||
logger.fatal(f"Could not fetch from URL. {await res.text()}")
|
||||
|
||||
|
||||
+29
-1
@@ -2,11 +2,15 @@ import re
|
||||
import ssl
|
||||
import subprocess
|
||||
import uuid
|
||||
import os
|
||||
from subprocess import check_output
|
||||
from time import sleep
|
||||
from hashlib import sha256
|
||||
from io import BytesIO
|
||||
|
||||
import certifi
|
||||
from aiohttp.web import Response, middleware
|
||||
from aiohttp import ClientSession
|
||||
|
||||
REMOTE_DEBUGGER_UNIT = "steam-web-debug-portforward.service"
|
||||
|
||||
@@ -27,7 +31,7 @@ def get_csrf_token():
|
||||
|
||||
@middleware
|
||||
async def csrf_middleware(request, handler):
|
||||
if str(request.method) == "OPTIONS" or request.headers.get('Authentication') == csrf_token or str(request.rel_url) == "/auth/token" or str(request.rel_url).startswith("/plugins/load_main/") or str(request.rel_url).startswith("/static/") or str(request.rel_url).startswith("/legacy/") or str(request.rel_url).startswith("/steam_resource/") or assets_regex.match(str(request.rel_url)) or frontend_regex.match(str(request.rel_url)):
|
||||
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 str(request.rel_url).startswith("/frontend/") or assets_regex.match(str(request.rel_url)) or frontend_regex.match(str(request.rel_url)):
|
||||
return await handler(request)
|
||||
return Response(text='Forbidden', status='403')
|
||||
|
||||
@@ -83,6 +87,30 @@ def get_homebrew_path(home_path = None) -> str:
|
||||
return str(home_path+"/homebrew")
|
||||
# return str(home_path+"/homebrew")
|
||||
|
||||
# Download Remote Binaries to local Plugin
|
||||
async def download_remote_binary_to_path(url, binHash, path) -> bool:
|
||||
rv = False
|
||||
try:
|
||||
if os.access(os.path.dirname(path), os.W_OK):
|
||||
async with ClientSession() as client:
|
||||
res = await client.get(url, ssl=get_ssl_context())
|
||||
if res.status == 200:
|
||||
data = BytesIO(await res.read())
|
||||
remoteHash = sha256(data.getbuffer()).hexdigest()
|
||||
if binHash == remoteHash:
|
||||
data.seek(0)
|
||||
with open(path, 'wb') as f:
|
||||
f.write(data.getbuffer())
|
||||
rv = True
|
||||
else:
|
||||
raise Exception(f"Fatal Error: Hash Mismatch for remote binary {path}@{url}")
|
||||
else:
|
||||
rv = False
|
||||
except:
|
||||
rv = False
|
||||
|
||||
return rv
|
||||
|
||||
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
|
||||
|
||||
+6
-1
@@ -25,8 +25,11 @@ class FileChangeHandler(RegexMatchingEventHandler):
|
||||
self.logger = getLogger("file-watcher")
|
||||
self.plugin_path = plugin_path
|
||||
self.queue = queue
|
||||
self.disabled = False
|
||||
|
||||
def maybe_reload(self, src_path):
|
||||
if self.disabled:
|
||||
return
|
||||
plugin_dir = Path(path.relpath(src_path, self.plugin_path)).parts[0]
|
||||
if exists(path.join(self.plugin_path, plugin_dir, "plugin.json")):
|
||||
self.queue.put_nowait((path.join(self.plugin_path, plugin_dir, "main.py"), plugin_dir, True))
|
||||
@@ -66,11 +69,13 @@ class Loader:
|
||||
self.plugin_path = plugin_path
|
||||
self.logger.info(f"plugin_path: {self.plugin_path}")
|
||||
self.plugins = {}
|
||||
self.watcher = None
|
||||
|
||||
if live_reload:
|
||||
self.reload_queue = Queue()
|
||||
self.observer = Observer()
|
||||
self.observer.schedule(FileChangeHandler(self.reload_queue, plugin_path), self.plugin_path, recursive=True)
|
||||
self.watcher = FileChangeHandler(self.reload_queue, plugin_path)
|
||||
self.observer.schedule(self.watcher, self.plugin_path, recursive=True)
|
||||
self.observer.start()
|
||||
self.loop.create_task(self.handle_reloads())
|
||||
|
||||
|
||||
+4
-3
@@ -4,6 +4,7 @@ from json import dumps, loads
|
||||
from logging import DEBUG, INFO, basicConfig, getLogger
|
||||
from os import getenv, path
|
||||
from subprocess import call
|
||||
from traceback import format_exc
|
||||
|
||||
import aiohttp_cors
|
||||
# Partial imports
|
||||
@@ -70,7 +71,7 @@ class PluginManager:
|
||||
)
|
||||
})
|
||||
self.plugin_loader = Loader(self.web_app, CONFIG["plugin_path"], self.loop, CONFIG["live_reload"])
|
||||
self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.plugin_loader.plugins)
|
||||
self.plugin_browser = PluginBrowser(CONFIG["plugin_path"], self.plugin_loader.plugins, self.plugin_loader)
|
||||
self.settings = SettingsManager("loader", path.join(HOMEBREW_PATH, "settings"))
|
||||
self.utilities = Utilities(self)
|
||||
self.updater = Updater(self)
|
||||
@@ -123,9 +124,9 @@ class PluginManager:
|
||||
|
||||
async def inject_javascript(self, request=None):
|
||||
try:
|
||||
await inject_to_tab("SP", "try{window.deckyHasLoaded = true;(async()=>{while(!window.SP_REACT){await new Promise(r => setTimeout(r, 10))};await import('http://localhost:1337/frontend/index.js')})();}catch(e){console.error(e)}", True)
|
||||
await inject_to_tab("SP", "try{if (window.deckyHasLoaded) location.reload();window.deckyHasLoaded = true;(async()=>{while(!window.SP_REACT){await new Promise(r => setTimeout(r, 10))};await import('http://localhost:1337/frontend/index.js')})();}catch(e){console.error(e)}", True)
|
||||
except:
|
||||
logger.info("Failed to inject JavaScript into tab")
|
||||
logger.info("Failed to inject JavaScript into tab\n" + format_exc())
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
|
||||
+22
-14
@@ -5,6 +5,8 @@ from asyncio import (Lock, get_event_loop, new_event_loop,
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from json import dumps, load, loads
|
||||
from logging import getLogger
|
||||
from traceback import format_exc
|
||||
from os import path, setgid, setuid
|
||||
from signal import SIGINT, signal
|
||||
from sys import exit
|
||||
@@ -40,28 +42,34 @@ class PluginWrapper:
|
||||
self.author = json["author"]
|
||||
self.flags = json["flags"]
|
||||
|
||||
self.log = getLogger("plugin")
|
||||
|
||||
self.passive = not path.isfile(self.file)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def _init(self):
|
||||
signal(SIGINT, lambda s, f: exit(0))
|
||||
try:
|
||||
signal(SIGINT, lambda s, f: exit(0))
|
||||
|
||||
set_event_loop(new_event_loop())
|
||||
if self.passive:
|
||||
return
|
||||
setgid(0 if "root" in self.flags else 1000)
|
||||
setuid(0 if "root" in self.flags else 1000)
|
||||
spec = spec_from_file_location("_", self.file)
|
||||
module = module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
self.Plugin = module.Plugin
|
||||
set_event_loop(new_event_loop())
|
||||
if self.passive:
|
||||
return
|
||||
setgid(0 if "root" in self.flags else 1000)
|
||||
setuid(0 if "root" in self.flags else 1000)
|
||||
spec = spec_from_file_location("_", self.file)
|
||||
module = module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
self.Plugin = module.Plugin
|
||||
|
||||
if hasattr(self.Plugin, "_main"):
|
||||
get_event_loop().create_task(self.Plugin._main(self.Plugin))
|
||||
get_event_loop().create_task(self._setup_socket())
|
||||
get_event_loop().run_forever()
|
||||
if hasattr(self.Plugin, "_main"):
|
||||
get_event_loop().create_task(self.Plugin._main(self.Plugin))
|
||||
get_event_loop().create_task(self._setup_socket())
|
||||
get_event_loop().run_forever()
|
||||
except:
|
||||
self.log.error("Failed to start " + self.name + "!\n" + format_exc())
|
||||
exit(0)
|
||||
|
||||
async def _setup_socket(self):
|
||||
self.socket = await start_unix_server(self._listen_for_method_call, path=self.socket_addr, limit=BUFFER_LIMIT)
|
||||
|
||||
+24
-12
@@ -20,7 +20,7 @@ class Updater:
|
||||
self.settings = self.context.settings
|
||||
# Exposes updater methods to frontend
|
||||
self.updater_methods = {
|
||||
"get_branch": self.get_branch,
|
||||
"get_branch": self._get_branch,
|
||||
"get_version": self.get_version,
|
||||
"do_update": self.do_update,
|
||||
"do_restart": self.do_restart,
|
||||
@@ -29,18 +29,18 @@ class Updater:
|
||||
self.remoteVer = None
|
||||
self.allRemoteVers = None
|
||||
try:
|
||||
self.currentBranch = self.get_branch(self.context.settings)
|
||||
if int(self.currentBranch) == -1:
|
||||
raise ValueError("get_branch could not determine branch!")
|
||||
except:
|
||||
self.currentBranch = 0
|
||||
logger.error("Current branch could not be determined, defaulting to \"Stable\"")
|
||||
try:
|
||||
logger.info(getcwd())
|
||||
with open(path.join(getcwd(), ".loader.version"), 'r') as version_file:
|
||||
self.localVer = version_file.readline().replace("\n", "")
|
||||
except:
|
||||
self.localVer = False
|
||||
|
||||
try:
|
||||
self.currentBranch = self.get_branch(self.context.settings)
|
||||
except:
|
||||
self.currentBranch = 0
|
||||
logger.error("Current branch could not be determined, defaulting to \"Stable\"")
|
||||
|
||||
if context:
|
||||
context.web_app.add_routes([
|
||||
web.post("/updater/{method_name}", self._handle_server_method_call)
|
||||
@@ -63,9 +63,21 @@ class Updater:
|
||||
res["success"] = False
|
||||
return web.json_response(res)
|
||||
|
||||
async def get_branch(self, manager: SettingsManager):
|
||||
logger.debug("current branch: %i" % manager.getSetting("branch", -1))
|
||||
return manager.getSetting("branch", -1)
|
||||
def get_branch(self, manager: SettingsManager):
|
||||
ver = manager.getSetting("branch", -1)
|
||||
logger.debug("current branch: %i" % ver)
|
||||
if ver == -1:
|
||||
logger.info("Current branch is not set, determining branch from version...")
|
||||
if self.localVer.startswith("v") and self.localVer.find("-pre"):
|
||||
logger.info("Current version determined to be pre-release")
|
||||
return 1
|
||||
else:
|
||||
logger.info("Current version determined to be stable")
|
||||
return 0
|
||||
return ver
|
||||
|
||||
async def _get_branch(self, manager: SettingsManager):
|
||||
return self.get_branch(manager)
|
||||
|
||||
async def get_version(self):
|
||||
if self.localVer:
|
||||
@@ -80,7 +92,7 @@ class Updater:
|
||||
|
||||
async def check_for_updates(self):
|
||||
logger.debug("checking for updates")
|
||||
selectedBranch = await self.get_branch(self.context.settings)
|
||||
selectedBranch = self.get_branch(self.context.settings)
|
||||
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()
|
||||
|
||||
+29
-1
@@ -1,4 +1,5 @@
|
||||
import uuid
|
||||
import os
|
||||
from json.decoder import JSONDecodeError
|
||||
|
||||
from aiohttp import ClientSession, web
|
||||
@@ -24,7 +25,8 @@ class Utilities:
|
||||
"allow_remote_debugging": self.allow_remote_debugging,
|
||||
"disallow_remote_debugging": self.disallow_remote_debugging,
|
||||
"set_setting": self.set_setting,
|
||||
"get_setting": self.get_setting
|
||||
"get_setting": self.get_setting,
|
||||
"filepicker_ls": self.filepicker_ls
|
||||
}
|
||||
|
||||
if context:
|
||||
@@ -166,3 +168,29 @@ class Utilities:
|
||||
async def disallow_remote_debugging(self):
|
||||
await helpers.stop_systemd_unit(helpers.REMOTE_DEBUGGER_UNIT)
|
||||
return True
|
||||
|
||||
async def filepicker_ls(self, path, include_files=True):
|
||||
# def sorter(file): # Modification time
|
||||
# if os.path.isdir(os.path.join(path, file)) or os.path.isfile(os.path.join(path, file)):
|
||||
# return os.path.getmtime(os.path.join(path, file))
|
||||
# return 0
|
||||
# file_names = sorted(os.listdir(path), key=sorter, reverse=True) # TODO provide more sort options
|
||||
file_names = sorted(os.listdir(path)) # Alphabetical
|
||||
|
||||
files = []
|
||||
|
||||
for file in file_names:
|
||||
full_path = os.path.join(path, file)
|
||||
is_dir = os.path.isdir(full_path)
|
||||
|
||||
if is_dir or include_files:
|
||||
files.append({
|
||||
"isdir": is_dir,
|
||||
"name": file,
|
||||
"realpath": os.path.realpath(full_path)
|
||||
})
|
||||
|
||||
return {
|
||||
"realpath": os.path.realpath(path),
|
||||
"files": files
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "decky_frontend",
|
||||
"version": "0.0.1",
|
||||
"version": "2.1.1",
|
||||
"private": true,
|
||||
"license": "GPLV2",
|
||||
"scripts": {
|
||||
@@ -17,6 +17,7 @@
|
||||
"@rollup/plugin-replace": "^4.0.0",
|
||||
"@rollup/plugin-typescript": "^8.3.3",
|
||||
"@types/react": "16.14.0",
|
||||
"@types/react-file-icon": "^1.0.1",
|
||||
"@types/react-router": "5.1.18",
|
||||
"@types/webpack": "^5.28.0",
|
||||
"husky": "^8.0.1",
|
||||
@@ -27,6 +28,7 @@
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"rollup": "^2.76.0",
|
||||
"rollup-plugin-delete": "^2.0.0",
|
||||
"rollup-plugin-external-globals": "^0.6.1",
|
||||
"rollup-plugin-polyfill-node": "^0.10.2",
|
||||
"tslib": "^2.4.0",
|
||||
@@ -39,7 +41,8 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"decky-frontend-lib": "^2.0.0",
|
||||
"decky-frontend-lib": "^3.1.3",
|
||||
"react-file-icon": "^1.2.0",
|
||||
"react-icons": "^4.4.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"remark-gfm": "^3.0.1"
|
||||
|
||||
Generated
+263
-6
@@ -7,9 +7,10 @@ specifiers:
|
||||
'@rollup/plugin-replace': ^4.0.0
|
||||
'@rollup/plugin-typescript': ^8.3.3
|
||||
'@types/react': 16.14.0
|
||||
'@types/react-file-icon': ^1.0.1
|
||||
'@types/react-router': 5.1.18
|
||||
'@types/webpack': ^5.28.0
|
||||
decky-frontend-lib: ^2.0.0
|
||||
decky-frontend-lib: ^3.1.3
|
||||
husky: ^8.0.1
|
||||
import-sort-style-module: ^6.0.0
|
||||
inquirer: ^8.2.4
|
||||
@@ -17,17 +18,20 @@ specifiers:
|
||||
prettier-plugin-import-sort: ^0.0.7
|
||||
react: 16.14.0
|
||||
react-dom: 16.14.0
|
||||
react-file-icon: ^1.2.0
|
||||
react-icons: ^4.4.0
|
||||
react-markdown: ^8.0.3
|
||||
remark-gfm: ^3.0.1
|
||||
rollup: ^2.76.0
|
||||
rollup-plugin-delete: ^2.0.0
|
||||
rollup-plugin-external-globals: ^0.6.1
|
||||
rollup-plugin-polyfill-node: ^0.10.2
|
||||
tslib: ^2.4.0
|
||||
typescript: ^4.7.4
|
||||
|
||||
dependencies:
|
||||
decky-frontend-lib: 2.0.0
|
||||
decky-frontend-lib: 3.1.3
|
||||
react-file-icon: 1.2.0_wcqkhtmu7mswc6yz4uyexck3ty
|
||||
react-icons: 4.4.0_react@16.14.0
|
||||
react-markdown: 8.0.3_vshvapmxg47tngu7tvrsqpq55u
|
||||
remark-gfm: 3.0.1
|
||||
@@ -39,6 +43,7 @@ devDependencies:
|
||||
'@rollup/plugin-replace': 4.0.0_rollup@2.76.0
|
||||
'@rollup/plugin-typescript': 8.3.3_mrkdcqv53wzt2ybukxlrvz47fu
|
||||
'@types/react': 16.14.0
|
||||
'@types/react-file-icon': 1.0.1
|
||||
'@types/react-router': 5.1.18
|
||||
'@types/webpack': 5.28.0
|
||||
husky: 8.0.1
|
||||
@@ -49,6 +54,7 @@ devDependencies:
|
||||
react: 16.14.0
|
||||
react-dom: 16.14.0_react@16.14.0
|
||||
rollup: 2.76.0
|
||||
rollup-plugin-delete: 2.0.0
|
||||
rollup-plugin-external-globals: 0.6.1_rollup@2.76.0
|
||||
rollup-plugin-polyfill-node: 0.10.2_rollup@2.76.0
|
||||
tslib: 2.4.0
|
||||
@@ -296,6 +302,27 @@ packages:
|
||||
'@jridgewell/sourcemap-codec': 1.4.14
|
||||
dev: true
|
||||
|
||||
/@nodelib/fs.scandir/2.1.5:
|
||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||
engines: {node: '>= 8'}
|
||||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
run-parallel: 1.2.0
|
||||
dev: true
|
||||
|
||||
/@nodelib/fs.stat/2.0.5:
|
||||
resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
|
||||
engines: {node: '>= 8'}
|
||||
dev: true
|
||||
|
||||
/@nodelib/fs.walk/1.2.8:
|
||||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||
engines: {node: '>= 8'}
|
||||
dependencies:
|
||||
'@nodelib/fs.scandir': 2.1.5
|
||||
fastq: 1.13.0
|
||||
dev: true
|
||||
|
||||
/@rollup/plugin-commonjs/21.1.0_rollup@2.76.0:
|
||||
resolution: {integrity: sha512-6ZtHx3VHIp2ReNNDxHjuUml6ur+WcQ28N1yHgCQwsbNkQg2suhxGMDQGJOn/KuDxKtd1xuZP5xSTwBA4GQ8hbA==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
@@ -427,6 +454,13 @@ packages:
|
||||
resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==}
|
||||
dev: true
|
||||
|
||||
/@types/glob/7.2.0:
|
||||
resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==}
|
||||
dependencies:
|
||||
'@types/minimatch': 5.1.2
|
||||
'@types/node': 18.0.4
|
||||
dev: true
|
||||
|
||||
/@types/hast/2.3.4:
|
||||
resolution: {integrity: sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==}
|
||||
dependencies:
|
||||
@@ -451,6 +485,10 @@ packages:
|
||||
resolution: {integrity: sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==}
|
||||
dev: false
|
||||
|
||||
/@types/minimatch/5.1.2:
|
||||
resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==}
|
||||
dev: true
|
||||
|
||||
/@types/ms/0.7.31:
|
||||
resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
|
||||
dev: false
|
||||
@@ -462,6 +500,12 @@ packages:
|
||||
/@types/prop-types/15.7.5:
|
||||
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
|
||||
|
||||
/@types/react-file-icon/1.0.1:
|
||||
resolution: {integrity: sha512-QTdYCkYXzh/PfKEIwcPxRdaPQkii5R4Ke7fcO+KB++IDPbYAG1jj+ulEcTA7pRf0gZ5jAvjWcTXBJJRtfYHjlw==}
|
||||
dependencies:
|
||||
'@types/react': 16.14.0
|
||||
dev: true
|
||||
|
||||
/@types/react-router/5.1.18:
|
||||
resolution: {integrity: sha512-YYknwy0D0iOwKQgz9v8nOzt2J6l4gouBmDnWqUUznltOTaon+r8US8ky8HvN0tXvc38U9m6z/t2RsVsnd1zM0g==}
|
||||
dependencies:
|
||||
@@ -626,6 +670,14 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/aggregate-error/3.1.0:
|
||||
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
clean-stack: 2.2.0
|
||||
indent-string: 4.0.0
|
||||
dev: true
|
||||
|
||||
/ajv-keywords/3.5.2_ajv@6.12.6:
|
||||
resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==}
|
||||
peerDependencies:
|
||||
@@ -675,6 +727,11 @@ packages:
|
||||
sprintf-js: 1.0.3
|
||||
dev: true
|
||||
|
||||
/array-union/2.1.0:
|
||||
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/bail/2.0.2:
|
||||
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
|
||||
dev: false
|
||||
@@ -702,6 +759,13 @@ packages:
|
||||
concat-map: 0.0.1
|
||||
dev: true
|
||||
|
||||
/braces/3.0.2:
|
||||
resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
fill-range: 7.0.1
|
||||
dev: true
|
||||
|
||||
/browserslist/4.21.2:
|
||||
resolution: {integrity: sha512-MonuOgAtUB46uP5CezYbRaYKBNt2LxP0yX+Pmj4LkcDFGkn9Cbpi83d9sCjwQDErXsIJSzY5oKGDbgOlF/LPAA==}
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
@@ -786,6 +850,11 @@ packages:
|
||||
engines: {node: '>=6.0'}
|
||||
dev: true
|
||||
|
||||
/clean-stack/2.2.0:
|
||||
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/cli-cursor/3.1.0:
|
||||
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -875,8 +944,8 @@ packages:
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
|
||||
/decky-frontend-lib/2.0.0:
|
||||
resolution: {integrity: sha512-H7+JpKHlClECVpo+MCEwej7R9wDWk9M2uMSyTvuhTfLZe3RThsxWCiqY640Cjh/zIW2A7GyVRd4SjLtn6Isdeg==}
|
||||
/decky-frontend-lib/3.1.3:
|
||||
resolution: {integrity: sha512-X6DGi90VdXnyoQi8Q4jEYhvBiNQualMNwXWKwgFitdor2ktNj5xp3a4uIi1ijbnS/vdW2AGKjraTfOMXtHUNAg==}
|
||||
dependencies:
|
||||
minimist: 1.2.6
|
||||
dev: false
|
||||
@@ -898,6 +967,20 @@ packages:
|
||||
clone: 1.0.4
|
||||
dev: true
|
||||
|
||||
/del/5.1.0:
|
||||
resolution: {integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
globby: 10.0.2
|
||||
graceful-fs: 4.2.10
|
||||
is-glob: 4.0.3
|
||||
is-path-cwd: 2.2.0
|
||||
is-path-inside: 3.0.3
|
||||
p-map: 3.0.0
|
||||
rimraf: 3.0.2
|
||||
slash: 3.0.0
|
||||
dev: true
|
||||
|
||||
/dequal/2.0.3:
|
||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -913,6 +996,13 @@ packages:
|
||||
engines: {node: '>=0.3.1'}
|
||||
dev: false
|
||||
|
||||
/dir-glob/3.0.1:
|
||||
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
path-type: 4.0.0
|
||||
dev: true
|
||||
|
||||
/electron-to-chromium/1.4.189:
|
||||
resolution: {integrity: sha512-dQ6Zn4ll2NofGtxPXaDfY2laIa6NyCQdqXYHdwH90GJQW0LpJJib0ZU/ERtbb0XkBEmUD2eJtagbOie3pdMiPg==}
|
||||
dev: true
|
||||
@@ -1015,10 +1105,27 @@ packages:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
dev: true
|
||||
|
||||
/fast-glob/3.2.11:
|
||||
resolution: {integrity: sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==}
|
||||
engines: {node: '>=8.6.0'}
|
||||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
'@nodelib/fs.walk': 1.2.8
|
||||
glob-parent: 5.1.2
|
||||
merge2: 1.4.1
|
||||
micromatch: 4.0.5
|
||||
dev: true
|
||||
|
||||
/fast-json-stable-stringify/2.1.0:
|
||||
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
|
||||
dev: true
|
||||
|
||||
/fastq/1.13.0:
|
||||
resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==}
|
||||
dependencies:
|
||||
reusify: 1.0.4
|
||||
dev: true
|
||||
|
||||
/figures/3.2.0:
|
||||
resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1026,6 +1133,13 @@ packages:
|
||||
escape-string-regexp: 1.0.5
|
||||
dev: true
|
||||
|
||||
/fill-range/7.0.1:
|
||||
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
dev: true
|
||||
|
||||
/find-line-column/0.5.2:
|
||||
resolution: {integrity: sha512-eNhNkDt5RbxY4X++JwyDURP62FYhV1bh9LF4dfOiwpVCTk5vvfEANhnui5ypUEELGR02QZSrWFtaTgd4ulW5tw==}
|
||||
dev: true
|
||||
@@ -1055,6 +1169,13 @@ packages:
|
||||
engines: {node: '>=6.9.0'}
|
||||
dev: true
|
||||
|
||||
/glob-parent/5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
engines: {node: '>= 6'}
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
dev: true
|
||||
|
||||
/glob-to-regexp/0.4.1:
|
||||
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
|
||||
dev: true
|
||||
@@ -1075,6 +1196,20 @@ packages:
|
||||
engines: {node: '>=4'}
|
||||
dev: true
|
||||
|
||||
/globby/10.0.2:
|
||||
resolution: {integrity: sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
'@types/glob': 7.2.0
|
||||
array-union: 2.1.0
|
||||
dir-glob: 3.0.1
|
||||
fast-glob: 3.2.11
|
||||
glob: 7.2.3
|
||||
ignore: 5.2.0
|
||||
merge2: 1.4.1
|
||||
slash: 3.0.0
|
||||
dev: true
|
||||
|
||||
/graceful-fs/4.2.10:
|
||||
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
|
||||
dev: true
|
||||
@@ -1117,6 +1252,11 @@ packages:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
dev: true
|
||||
|
||||
/ignore/5.2.0:
|
||||
resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==}
|
||||
engines: {node: '>= 4'}
|
||||
dev: true
|
||||
|
||||
/import-fresh/2.0.0:
|
||||
resolution: {integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -1174,6 +1314,11 @@ packages:
|
||||
resolve: 1.22.1
|
||||
dev: true
|
||||
|
||||
/indent-string/4.0.0:
|
||||
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/inflight/1.0.6:
|
||||
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
|
||||
dependencies:
|
||||
@@ -1237,11 +1382,23 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/is-extglob/2.1.1:
|
||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/is-fullwidth-code-point/3.0.0:
|
||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/is-glob/4.0.3:
|
||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dependencies:
|
||||
is-extglob: 2.1.1
|
||||
dev: true
|
||||
|
||||
/is-interactive/1.0.0:
|
||||
resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1251,6 +1408,21 @@ packages:
|
||||
resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==}
|
||||
dev: true
|
||||
|
||||
/is-number/7.0.0:
|
||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||
engines: {node: '>=0.12.0'}
|
||||
dev: true
|
||||
|
||||
/is-path-cwd/2.2.0:
|
||||
resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==}
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/is-path-inside/3.0.3:
|
||||
resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/is-plain-obj/4.1.0:
|
||||
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1321,6 +1493,10 @@ packages:
|
||||
engines: {node: '>=6.11.5'}
|
||||
dev: true
|
||||
|
||||
/lodash.uniqueid/4.0.1:
|
||||
resolution: {integrity: sha512-GQQWaIeGlL6DIIr06kj1j6sSmBxyNMwI8kaX9aKpHR/XsMTiaXDVPNPAkiboOTK9OJpTJF/dXT3xYoFQnj386Q==}
|
||||
dev: false
|
||||
|
||||
/lodash/4.17.21:
|
||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||
dev: true
|
||||
@@ -1483,6 +1659,11 @@ packages:
|
||||
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
||||
dev: true
|
||||
|
||||
/merge2/1.4.1:
|
||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||
engines: {node: '>= 8'}
|
||||
dev: true
|
||||
|
||||
/micromark-core-commonmark/1.0.6:
|
||||
resolution: {integrity: sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==}
|
||||
dependencies:
|
||||
@@ -1732,6 +1913,14 @@ packages:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/micromatch/4.0.5:
|
||||
resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
|
||||
engines: {node: '>=8.6'}
|
||||
dependencies:
|
||||
braces: 3.0.2
|
||||
picomatch: 2.3.1
|
||||
dev: true
|
||||
|
||||
/mime-db/1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -1816,6 +2005,13 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/p-map/3.0.0:
|
||||
resolution: {integrity: sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
aggregate-error: 3.1.0
|
||||
dev: true
|
||||
|
||||
/parse-json/4.0.0:
|
||||
resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -1833,6 +2029,11 @@ packages:
|
||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||
dev: true
|
||||
|
||||
/path-type/4.0.0:
|
||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/picocolors/1.0.0:
|
||||
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
|
||||
dev: true
|
||||
@@ -1878,6 +2079,10 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/queue-microtask/1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
dev: true
|
||||
|
||||
/randombytes/2.1.0:
|
||||
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
||||
dependencies:
|
||||
@@ -1894,7 +2099,19 @@ packages:
|
||||
prop-types: 15.8.1
|
||||
react: 16.14.0
|
||||
scheduler: 0.19.1
|
||||
dev: true
|
||||
|
||||
/react-file-icon/1.2.0_wcqkhtmu7mswc6yz4uyexck3ty:
|
||||
resolution: {integrity: sha512-BI8CTyZu/k8AmhjGJiGYOqgjfp2si2Lt5PUNF6kfF31c7BFYJeerpfHnZBfpPjrb2M/DAdW1qNub17Rt+xuefQ==}
|
||||
peerDependencies:
|
||||
react: ^18.0.0 || ^17.0.0 || ^16.2.0
|
||||
react-dom: ^18.0.0 || ^17.0.0 || ^16.2.0
|
||||
dependencies:
|
||||
lodash.uniqueid: 4.0.1
|
||||
prop-types: 15.8.1
|
||||
react: 16.14.0
|
||||
react-dom: 16.14.0_react@16.14.0
|
||||
tinycolor2: 1.4.2
|
||||
dev: false
|
||||
|
||||
/react-icons/4.4.0_react@16.14.0:
|
||||
resolution: {integrity: sha512-fSbvHeVYo/B5/L4VhB7sBA1i2tS8MkT0Hb9t2H1AVPkwGfVHLJCqyr2Py9dKMxsyM63Eng1GkdZfbWj+Fmv8Rg==}
|
||||
@@ -2012,6 +2229,25 @@ packages:
|
||||
signal-exit: 3.0.7
|
||||
dev: true
|
||||
|
||||
/reusify/1.0.4:
|
||||
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
|
||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/rimraf/3.0.2:
|
||||
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
glob: 7.2.3
|
||||
dev: true
|
||||
|
||||
/rollup-plugin-delete/2.0.0:
|
||||
resolution: {integrity: sha512-/VpLMtDy+8wwRlDANuYmDa9ss/knGsAgrDhM+tEwB1npHwNu4DYNmDfUL55csse/GHs9Q+SMT/rw9uiaZ3pnzA==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
del: 5.1.0
|
||||
dev: true
|
||||
|
||||
/rollup-plugin-external-globals/0.6.1_rollup@2.76.0:
|
||||
resolution: {integrity: sha512-mlp3KNa5sE4Sp9UUR2rjBrxjG79OyZAh/QC18RHIjM+iYkbBwNXSo8DHRMZWtzJTrH8GxQ+SJvCTN3i14uMXIA==}
|
||||
peerDependencies:
|
||||
@@ -2046,6 +2282,12 @@ packages:
|
||||
engines: {node: '>=0.12.0'}
|
||||
dev: true
|
||||
|
||||
/run-parallel/1.2.0:
|
||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||
dependencies:
|
||||
queue-microtask: 1.2.3
|
||||
dev: true
|
||||
|
||||
/rxjs/7.5.6:
|
||||
resolution: {integrity: sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==}
|
||||
dependencies:
|
||||
@@ -2076,7 +2318,6 @@ packages:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
object-assign: 4.1.1
|
||||
dev: true
|
||||
|
||||
/schema-utils/3.1.1:
|
||||
resolution: {integrity: sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==}
|
||||
@@ -2102,6 +2343,11 @@ packages:
|
||||
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
|
||||
dev: true
|
||||
|
||||
/slash/3.0.0:
|
||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/source-map-support/0.5.21:
|
||||
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
|
||||
dependencies:
|
||||
@@ -2224,6 +2470,10 @@ packages:
|
||||
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
||||
dev: true
|
||||
|
||||
/tinycolor2/1.4.2:
|
||||
resolution: {integrity: sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==}
|
||||
dev: false
|
||||
|
||||
/tmp/0.0.33:
|
||||
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
|
||||
engines: {node: '>=0.6.0'}
|
||||
@@ -2236,6 +2486,13 @@ packages:
|
||||
engines: {node: '>=4'}
|
||||
dev: true
|
||||
|
||||
/to-regex-range/5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
dependencies:
|
||||
is-number: 7.0.0
|
||||
dev: true
|
||||
|
||||
/trim-lines/3.0.1:
|
||||
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
|
||||
dev: false
|
||||
|
||||
@@ -2,6 +2,7 @@ import commonjs from '@rollup/plugin-commonjs';
|
||||
import json from '@rollup/plugin-json';
|
||||
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
||||
import externalGlobals from "rollup-plugin-external-globals";
|
||||
import del from 'rollup-plugin-delete'
|
||||
import replace from '@rollup/plugin-replace';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import { defineConfig } from 'rollup';
|
||||
@@ -9,6 +10,7 @@ import { defineConfig } from 'rollup';
|
||||
export default defineConfig({
|
||||
input: 'src/index.tsx',
|
||||
plugins: [
|
||||
del({ targets: "../backend/static/*", force: true }),
|
||||
commonjs(),
|
||||
nodeResolve(),
|
||||
externalGlobals({
|
||||
|
||||
@@ -1,9 +1,41 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
import { Focusable } from 'decky-frontend-lib';
|
||||
import { FunctionComponent, useRef } from 'react';
|
||||
import ReactMarkdown, { Options as ReactMarkdownOptions } from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
|
||||
const Markdown: FunctionComponent<ReactMarkdownOptions> = (props) => {
|
||||
return <ReactMarkdown remarkPlugins={[remarkGfm]} {...props} />;
|
||||
interface MarkdownProps extends ReactMarkdownOptions {
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
const Markdown: FunctionComponent<MarkdownProps> = (props) => {
|
||||
return (
|
||||
<Focusable>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
div: (nodeProps) => <Focusable {...nodeProps.node.properties}>{nodeProps.children}</Focusable>,
|
||||
a: (nodeProps) => {
|
||||
const aRef = useRef<HTMLAnchorElement>(null);
|
||||
return (
|
||||
// TODO fix focus ring
|
||||
<Focusable
|
||||
onActivate={() => {}}
|
||||
onOKButton={() => {
|
||||
aRef?.current?.click();
|
||||
props.onDismiss?.();
|
||||
}}
|
||||
>
|
||||
<a ref={aRef} {...nodeProps.node.properties}>
|
||||
{nodeProps.children}
|
||||
</a>
|
||||
</Focusable>
|
||||
);
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
</Focusable>
|
||||
);
|
||||
};
|
||||
|
||||
export default Markdown;
|
||||
|
||||
@@ -32,8 +32,8 @@ const PluginView: VFC = () => {
|
||||
.map(({ name, icon }) => (
|
||||
<PanelSectionRow key={name}>
|
||||
<ButtonItem layout="below" onClick={() => setActivePlugin(name)}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>{icon}</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
{icon}
|
||||
<div>{name}</div>
|
||||
<NotificationBadge show={updates?.has(name)} style={{ top: '-5px', right: '-5px' }} />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ToastData, findModule, findModuleChild, joinClassNames } from 'decky-frontend-lib';
|
||||
import { FunctionComponent, useEffect } from 'react';
|
||||
import { ToastData, findModule, joinClassNames } from 'decky-frontend-lib';
|
||||
import { FunctionComponent } from 'react';
|
||||
|
||||
interface ToastProps {
|
||||
toast: {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Focusable, SteamSpinner } from 'decky-frontend-lib';
|
||||
import { FunctionComponent, ReactElement, ReactNode, Suspense } from 'react';
|
||||
|
||||
interface WithSuspenseProps {
|
||||
children: ReactNode;
|
||||
route?: boolean;
|
||||
}
|
||||
|
||||
// Nice little wrapper around Suspense so we don't have to duplicate the styles and code for the loading spinner
|
||||
const WithSuspense: FunctionComponent<WithSuspenseProps> = (props) => {
|
||||
const propsCopy = { ...props };
|
||||
delete propsCopy.children;
|
||||
(props.children as ReactElement)?.props && Object.assign((props.children as ReactElement).props, propsCopy); // There is probably a better way to do this but valve does it this way so ¯\_(ツ)_/¯
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<Focusable
|
||||
// needed to enable focus ring so that the focus properly resets on load
|
||||
onActivate={() => {}}
|
||||
style={{
|
||||
overflowY: 'scroll',
|
||||
backgroundColor: 'transparent',
|
||||
...(props.route && {
|
||||
marginTop: '40px',
|
||||
height: 'calc( 100% - 40px )',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<SteamSpinner />
|
||||
</Focusable>
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default WithSuspense;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ModalRoot, QuickAccessTab, Router, Spinner, staticClasses } from 'decky-frontend-lib';
|
||||
import { ConfirmModal, QuickAccessTab, Router, Spinner, staticClasses } from 'decky-frontend-lib';
|
||||
import { FC, useState } from 'react';
|
||||
|
||||
interface PluginInstallModalProps {
|
||||
@@ -14,13 +14,14 @@ interface PluginInstallModalProps {
|
||||
const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, hash, onOK, onCancel, closeModal }) => {
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
return (
|
||||
<ModalRoot
|
||||
<ConfirmModal
|
||||
bOKDisabled={loading}
|
||||
closeModal={closeModal}
|
||||
onOK={async () => {
|
||||
setLoading(true);
|
||||
await onOK();
|
||||
setTimeout(() => Router.OpenQuickAccessMenu(QuickAccessTab.Decky), 250);
|
||||
setTimeout(() => window.DeckyPluginLoader.checkPluginUpdates(), 1000);
|
||||
}}
|
||||
onCancel={async () => {
|
||||
await onCancel();
|
||||
@@ -34,7 +35,7 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ artifact, version, ha
|
||||
{!loading && '?'}
|
||||
</div>
|
||||
</div>
|
||||
</ModalRoot>
|
||||
</ConfirmModal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
// https://codesandbox.io/s/react-file-icon-colored-tmwut?file=/src/App.js
|
||||
import { FileIconProps } from 'react-file-icon';
|
||||
|
||||
type T_FileExtList = string[];
|
||||
|
||||
const styleDef: [FileIconProps, T_FileExtList][] = [];
|
||||
|
||||
// video ////////////////////////////////////
|
||||
const videoStyle = {
|
||||
color: '#f00f0f',
|
||||
};
|
||||
const videoExtList = [
|
||||
'avi',
|
||||
'3g2',
|
||||
'3gp',
|
||||
'aep',
|
||||
'asf',
|
||||
'flv',
|
||||
'm4v',
|
||||
'mkv',
|
||||
'mov',
|
||||
'mp4',
|
||||
'mpeg',
|
||||
'mpg',
|
||||
'ogv',
|
||||
'pr',
|
||||
'swfw',
|
||||
'webm',
|
||||
'wmv',
|
||||
'swf',
|
||||
'rm',
|
||||
];
|
||||
|
||||
styleDef.push([videoStyle, videoExtList]);
|
||||
|
||||
// image ////////////////////////////////////
|
||||
const imageStyle = {
|
||||
color: '#d18f00',
|
||||
};
|
||||
|
||||
const imageExtList = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tif', 'tiff'];
|
||||
|
||||
styleDef.push([imageStyle, imageExtList]);
|
||||
|
||||
// zip ////////////////////////////////////
|
||||
const zipStyle = {
|
||||
color: '#f7b500',
|
||||
labelTextColor: '#000',
|
||||
// glyphColor: "#de9400"
|
||||
};
|
||||
|
||||
const zipExtList = ['zip', 'zipx', '7zip', 'tar', 'sitx', 'gz', 'rar'];
|
||||
|
||||
styleDef.push([zipStyle, zipExtList]);
|
||||
|
||||
// audio ////////////////////////////////////
|
||||
const audioStyle = {
|
||||
color: '#f00f0f',
|
||||
};
|
||||
|
||||
const audioExtList = ['aac', 'aif', 'aiff', 'flac', 'm4a', 'mid', 'mp3', 'ogg', 'wav'];
|
||||
|
||||
styleDef.push([audioStyle, audioExtList]);
|
||||
|
||||
// text ////////////////////////////////////
|
||||
const textStyle = {
|
||||
color: '#ffffff',
|
||||
glyphColor: '#787878',
|
||||
};
|
||||
|
||||
const textExtList = ['cue', 'odt', 'md', 'rtf', 'txt', 'tex', 'wpd', 'wps', 'xlr', 'fodt'];
|
||||
|
||||
styleDef.push([textStyle, textExtList]);
|
||||
|
||||
// system ////////////////////////////////////
|
||||
const systemStyle = {
|
||||
color: '#111',
|
||||
};
|
||||
|
||||
const systemExtList = ['exe', 'ini', 'dll', 'plist', 'sys'];
|
||||
|
||||
styleDef.push([systemStyle, systemExtList]);
|
||||
|
||||
// srcCode ////////////////////////////////////
|
||||
const srcCodeStyle = {
|
||||
glyphColor: '#787878',
|
||||
color: '#ffffff',
|
||||
};
|
||||
|
||||
const srcCodeExtList = [
|
||||
'asp',
|
||||
'aspx',
|
||||
'c',
|
||||
'cpp',
|
||||
'cs',
|
||||
'css',
|
||||
'scss',
|
||||
'py',
|
||||
'json',
|
||||
'htm',
|
||||
'html',
|
||||
'java',
|
||||
'yml',
|
||||
'php',
|
||||
'js',
|
||||
'ts',
|
||||
'rb',
|
||||
'jsx',
|
||||
'tsx',
|
||||
];
|
||||
|
||||
styleDef.push([srcCodeStyle, srcCodeExtList]);
|
||||
|
||||
// vector ////////////////////////////////////
|
||||
const vectorStyle = {
|
||||
color: '#ffe600',
|
||||
};
|
||||
|
||||
const vectorExtList = ['dwg', 'dxf', 'ps', 'svg', 'eps'];
|
||||
|
||||
styleDef.push([vectorStyle, vectorExtList]);
|
||||
|
||||
// font ////////////////////////////////////
|
||||
const fontStyle = {
|
||||
color: '#555',
|
||||
};
|
||||
|
||||
const fontExtList = ['fnt', 'ttf', 'otf', 'fon', 'eot', 'woff'];
|
||||
|
||||
styleDef.push([fontStyle, fontExtList]);
|
||||
|
||||
// objectModel ////////////////////////////////////
|
||||
const objectModelStyle = {
|
||||
color: '#bf6a02',
|
||||
glyphColor: '#bf6a02',
|
||||
};
|
||||
|
||||
const objectModelExtList = ['3dm', '3ds', 'max', 'obj', 'pkg'];
|
||||
|
||||
styleDef.push([objectModelStyle, objectModelExtList]);
|
||||
|
||||
// sheet ////////////////////////////////////
|
||||
const sheetStyle = {
|
||||
color: '#2a6e00',
|
||||
};
|
||||
|
||||
const sheetExtList = ['csv', 'fods', 'ods', 'xlr'];
|
||||
|
||||
styleDef.push([sheetStyle, sheetExtList]);
|
||||
|
||||
// const defaultStyle: Record<string, FileIconProps> = {
|
||||
// pdf: {
|
||||
// glyphColor: "white",
|
||||
// color: "#D93831"
|
||||
// }
|
||||
// };
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
|
||||
function createStyleObj(extList: T_FileExtList, styleObj: Partial<FileIconProps>) {
|
||||
return Object.fromEntries(
|
||||
extList.map((ext) => {
|
||||
return [ext, { ...styleObj, glyphColor: 'white' }];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export const styleDefObj = styleDef.reduce((acc, [fileStyle, fileExtList]) => {
|
||||
return { ...acc, ...createStyleObj(fileExtList, fileStyle) };
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
import { DialogButton, Focusable, SteamSpinner, TextField } from 'decky-frontend-lib';
|
||||
import { useEffect } from 'react';
|
||||
import { FunctionComponent, useState } from 'react';
|
||||
import { FileIcon, defaultStyles } from 'react-file-icon';
|
||||
import { FaArrowUp, FaFolder } from 'react-icons/fa';
|
||||
|
||||
import Logger from '../../../logger';
|
||||
import { styleDefObj } from './iconCustomizations';
|
||||
|
||||
const logger = new Logger('FilePicker');
|
||||
|
||||
export interface FilePickerProps {
|
||||
startPath: string;
|
||||
includeFiles?: boolean;
|
||||
regex?: RegExp;
|
||||
onSubmit: (val: { path: string; realpath: string }) => void;
|
||||
closeModal?: () => void;
|
||||
}
|
||||
|
||||
interface File {
|
||||
isdir: boolean;
|
||||
name: string;
|
||||
realpath: string;
|
||||
}
|
||||
|
||||
interface FileListing {
|
||||
realpath: string;
|
||||
files: File[];
|
||||
}
|
||||
|
||||
function getList(
|
||||
path: string,
|
||||
includeFiles: boolean = true,
|
||||
): Promise<{ result: FileListing | string; success: boolean }> {
|
||||
return window.DeckyPluginLoader.callServerMethod('filepicker_ls', { path, include_files: includeFiles });
|
||||
}
|
||||
|
||||
const iconStyles = {
|
||||
paddingRight: '10px',
|
||||
width: '1em',
|
||||
};
|
||||
|
||||
const FilePicker: FunctionComponent<FilePickerProps> = ({
|
||||
startPath,
|
||||
includeFiles = true,
|
||||
regex,
|
||||
onSubmit,
|
||||
closeModal,
|
||||
}) => {
|
||||
if (startPath.endsWith('/')) startPath = startPath.substring(0, startPath.length - 1); // remove trailing path
|
||||
const [path, setPath] = useState<string>(startPath);
|
||||
const [listing, setListing] = useState<FileListing>({ files: [], realpath: path });
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (error) setError(null);
|
||||
setLoading(true);
|
||||
const listing = await getList(path, includeFiles);
|
||||
if (!listing.success) {
|
||||
setListing({ files: [], realpath: path });
|
||||
setLoading(false);
|
||||
setError(listing.result as string);
|
||||
logger.error(listing.result);
|
||||
return;
|
||||
}
|
||||
setLoading(false);
|
||||
setListing(listing.result as FileListing);
|
||||
logger.log('reloaded', path, listing);
|
||||
})();
|
||||
}, [path]);
|
||||
|
||||
return (
|
||||
<div className="deckyFilePicker">
|
||||
<Focusable style={{ display: 'flex', flexDirection: 'row', paddingBottom: '10px' }}>
|
||||
<DialogButton
|
||||
style={{
|
||||
minWidth: 'unset',
|
||||
width: '40px',
|
||||
flexGrow: '0',
|
||||
borderRadius: 'unset',
|
||||
margin: '0',
|
||||
padding: '10px',
|
||||
}}
|
||||
onClick={() => {
|
||||
const newPathArr = path.split('/');
|
||||
newPathArr.pop();
|
||||
let newPath = newPathArr.join('/');
|
||||
if (newPath == '') newPath = '/';
|
||||
setPath(newPath);
|
||||
}}
|
||||
>
|
||||
<FaArrowUp />
|
||||
</DialogButton>
|
||||
<div style={{ flexGrow: '1', width: '100%' }}>
|
||||
<TextField
|
||||
value={path}
|
||||
onChange={(e) => {
|
||||
e.target.value && setPath(e.target.value);
|
||||
}}
|
||||
style={{ height: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
</Focusable>
|
||||
<Focusable style={{ display: 'flex', flexDirection: 'column', height: '60vh', overflow: 'scroll' }}>
|
||||
{loading && <SteamSpinner style={{ height: '100%' }} />}
|
||||
{!loading &&
|
||||
listing.files
|
||||
.filter((file) => (includeFiles || file.isdir) && (!regex || regex.test(file.name)))
|
||||
.map((file) => {
|
||||
let extension = file.realpath.split('.').pop() as string;
|
||||
return (
|
||||
<DialogButton
|
||||
style={{ borderRadius: 'unset', margin: '0', padding: '10px' }}
|
||||
onClick={() => {
|
||||
const fullPath = `${path}${path.endsWith('/') ? '' : '/'}${file.name}`;
|
||||
if (file.isdir) setPath(fullPath);
|
||||
else {
|
||||
onSubmit({ path: fullPath, realpath: file.realpath });
|
||||
closeModal?.();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'flex-start' }}>
|
||||
{file.isdir ? (
|
||||
<FaFolder style={iconStyles} />
|
||||
) : (
|
||||
<div style={iconStyles}>
|
||||
{file.realpath.includes('.') ? (
|
||||
<FileIcon {...defaultStyles[extension]} {...styleDefObj[extension]} extension={''} />
|
||||
) : (
|
||||
<FileIcon />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{file.name}
|
||||
</div>
|
||||
</DialogButton>
|
||||
);
|
||||
})}
|
||||
{error}
|
||||
</Focusable>
|
||||
{!loading && !error && !includeFiles && (
|
||||
<DialogButton
|
||||
className="Primary"
|
||||
style={{ marginTop: '10px', alignSelf: 'flex-end' }}
|
||||
onClick={() => {
|
||||
onSubmit({ path, realpath: listing.realpath });
|
||||
closeModal?.();
|
||||
}}
|
||||
>
|
||||
Use this folder
|
||||
</DialogButton>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilePicker;
|
||||
@@ -0,0 +1 @@
|
||||
This directory contains patches that replace Valve's broken file picker with ours.
|
||||
@@ -0,0 +1,10 @@
|
||||
import library from './library';
|
||||
let patches: Function[] = [];
|
||||
|
||||
export function deinitFilepickerPatches() {
|
||||
patches.forEach((unpatch) => unpatch());
|
||||
}
|
||||
|
||||
export async function initFilepickerPatches() {
|
||||
patches.push(await library());
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Patch, findModuleChild, replacePatch } from 'decky-frontend-lib';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
SteamClient: any;
|
||||
appDetailsStore: any;
|
||||
}
|
||||
}
|
||||
|
||||
let patch: Patch;
|
||||
|
||||
function rePatch() {
|
||||
// If you patch anything on SteamClient within the first few seconds of the client having loaded it will get redefined for some reason, so repatch any of these changes that occur within the first 20s of the last patch
|
||||
patch = replacePatch(window.SteamClient.Apps, 'PromptToChangeShortcut', async ([appid]: number[]) => {
|
||||
try {
|
||||
const details = window.appDetailsStore.GetAppDetails(appid);
|
||||
console.log(details);
|
||||
// strShortcutStartDir
|
||||
const file = await window.DeckyPluginLoader.openFilePicker(details.strShortcutStartDir.replaceAll('"', ''));
|
||||
console.log('user selected', file);
|
||||
window.SteamClient.Apps.SetShortcutExe(appid, JSON.stringify(file.path));
|
||||
const pathArr = file.path.split('/');
|
||||
pathArr.pop();
|
||||
const folder = pathArr.join('/');
|
||||
window.SteamClient.Apps.SetShortcutStartDir(appid, JSON.stringify(folder));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO type and add to frontend-lib
|
||||
const History = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.m_history) return m[prop].m_history;
|
||||
}
|
||||
});
|
||||
|
||||
export default async function libraryPatch() {
|
||||
try {
|
||||
rePatch();
|
||||
const unlisten = History.listen(() => {
|
||||
if (window.SteamClient.Apps.PromptToChangeShortcut !== patch.patchedFunction) {
|
||||
rePatch();
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
patch.unpatch();
|
||||
unlisten();
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Error patching library file picker', e);
|
||||
}
|
||||
return () => {};
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Dropdown, Field } from 'decky-frontend-lib';
|
||||
import { FunctionComponent } from 'react';
|
||||
|
||||
import Logger from '../../../../logger';
|
||||
import { callUpdaterMethod } from '../../../../updater';
|
||||
import { useSetting } from '../../../../utils/hooks/useSetting';
|
||||
|
||||
const logger = new Logger('BranchSelect');
|
||||
|
||||
enum UpdateBranch {
|
||||
Stable,
|
||||
Prerelease,
|
||||
@@ -28,7 +31,7 @@ const BranchSelect: FunctionComponent<{}> = () => {
|
||||
onChange={async (newVal) => {
|
||||
await setSelectedBranch(newVal.data);
|
||||
callUpdaterMethod('check_for_updates');
|
||||
console.log('switching branches!');
|
||||
logger.log('switching branches!');
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { Carousel, DialogButton, Field, Focusable, ProgressBarWithInfo, Spinner, showModal } from 'decky-frontend-lib';
|
||||
import {
|
||||
Carousel,
|
||||
DialogButton,
|
||||
Field,
|
||||
FocusRing,
|
||||
Focusable,
|
||||
ProgressBarWithInfo,
|
||||
Spinner,
|
||||
showModal,
|
||||
} from 'decky-frontend-lib';
|
||||
import { useCallback } from 'react';
|
||||
import { Suspense, lazy } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -7,49 +16,48 @@ import { FaArrowDown } from 'react-icons/fa';
|
||||
import { VerInfo, callUpdaterMethod, finishUpdate } from '../../../../updater';
|
||||
import { useDeckyState } from '../../../DeckyState';
|
||||
import InlinePatchNotes from '../../../patchnotes/InlinePatchNotes';
|
||||
import WithSuspense from '../../../WithSuspense';
|
||||
|
||||
const MarkdownRenderer = lazy(() => import('../../../Markdown'));
|
||||
|
||||
// import ReactMarkdown from 'react-markdown'
|
||||
// import remarkGfm from 'remark-gfm'
|
||||
|
||||
function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | null; closeModal?: () => {} }) {
|
||||
return (
|
||||
<Focusable onCancelButton={closeModal}>
|
||||
<Carousel
|
||||
fnItemRenderer={(id: number) => (
|
||||
<Focusable
|
||||
onActivate={() => {}}
|
||||
style={{
|
||||
marginTop: '40px',
|
||||
height: 'calc( 100% - 40px )',
|
||||
overflowY: 'scroll',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
margin: '40px',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h1>{versionInfo?.all?.[id]?.name}</h1>
|
||||
{versionInfo?.all?.[id]?.body ? (
|
||||
<Suspense fallback={<Spinner style={{ width: '24', height: '24' }} />}>
|
||||
<MarkdownRenderer>{versionInfo.all[id].body}</MarkdownRenderer>
|
||||
</Suspense>
|
||||
) : (
|
||||
'no patch notes for this version'
|
||||
)}
|
||||
</div>
|
||||
</Focusable>
|
||||
)}
|
||||
fnGetId={(id) => id}
|
||||
nNumItems={versionInfo?.all?.length}
|
||||
nHeight={window.innerHeight - 150}
|
||||
nItemHeight={window.innerHeight - 200}
|
||||
nItemMarginX={0}
|
||||
initialColumn={0}
|
||||
autoFocus={true}
|
||||
fnGetColumnWidth={() => window.innerWidth}
|
||||
/>
|
||||
<FocusRing>
|
||||
<Carousel
|
||||
fnItemRenderer={(id: number) => (
|
||||
<Focusable
|
||||
style={{
|
||||
marginTop: '40px',
|
||||
height: 'calc( 100% - 40px )',
|
||||
overflowY: 'scroll',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
margin: '40px',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h1>{versionInfo?.all?.[id]?.name}</h1>
|
||||
{versionInfo?.all?.[id]?.body ? (
|
||||
<WithSuspense>
|
||||
<MarkdownRenderer onDismiss={closeModal}>{versionInfo.all[id].body}</MarkdownRenderer>
|
||||
</WithSuspense>
|
||||
) : (
|
||||
'no patch notes for this version'
|
||||
)}
|
||||
</div>
|
||||
</Focusable>
|
||||
)}
|
||||
fnGetId={(id) => id}
|
||||
nNumItems={versionInfo?.all?.length}
|
||||
nHeight={window.innerHeight - 40}
|
||||
nItemHeight={window.innerHeight - 40}
|
||||
nItemMarginX={0}
|
||||
initialColumn={0}
|
||||
autoFocus={true}
|
||||
fnGetColumnWidth={() => window.innerWidth}
|
||||
/>
|
||||
</FocusRing>
|
||||
</Focusable>
|
||||
);
|
||||
}
|
||||
@@ -126,7 +134,7 @@ export default function UpdaterSettings() {
|
||||
) : (
|
||||
<ProgressBarWithInfo
|
||||
layout="inline"
|
||||
bottomSeparator={false}
|
||||
bottomSeparator="none"
|
||||
nProgress={updateProgress}
|
||||
indeterminate={reloading}
|
||||
sOperationText={reloading ? 'Reloading' : 'Updating'}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { SteamSpinner } from 'decky-frontend-lib';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
|
||||
import Logger from '../../logger';
|
||||
import { LegacyStorePlugin, StorePlugin, getLegacyPluginList, getPluginList } from '../../store';
|
||||
import PluginCard from './PluginCard';
|
||||
|
||||
const logger = new Logger('FilePicker');
|
||||
|
||||
const StorePage: FC<{}> = () => {
|
||||
const [data, setData] = useState<StorePlugin[] | null>(null);
|
||||
const [legacyData, setLegacyData] = useState<LegacyStorePlugin[] | null>(null);
|
||||
@@ -11,12 +14,12 @@ const StorePage: FC<{}> = () => {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const res = await getPluginList();
|
||||
console.log(res);
|
||||
logger.log('got data!', res);
|
||||
setData(res);
|
||||
})();
|
||||
(async () => {
|
||||
const res = await getLegacyPluginList();
|
||||
console.log(res);
|
||||
logger.log('got legacy data!', res);
|
||||
setLegacyData(res);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
@@ -19,7 +19,7 @@ export const debug = (name: string, ...args: any[]) => {
|
||||
};
|
||||
|
||||
export const error = (name: string, ...args: any[]) => {
|
||||
console.log(
|
||||
console.error(
|
||||
`%c Decky %c ${name} %c`,
|
||||
'background: #16a085; color: black;',
|
||||
'background: #FF0000;',
|
||||
@@ -40,6 +40,10 @@ class Logger {
|
||||
debug(...args: any[]) {
|
||||
debug(this.name, ...args);
|
||||
}
|
||||
|
||||
error(...args: any[]) {
|
||||
error(this.name, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
export default Logger;
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { ModalRoot, QuickAccessTab, Router, SteamSpinner, showModal, sleep, staticClasses } from 'decky-frontend-lib';
|
||||
import { Suspense, lazy } from 'react';
|
||||
import { ConfirmModal, ModalRoot, QuickAccessTab, Router, showModal, sleep, staticClasses } from 'decky-frontend-lib';
|
||||
import { lazy } from 'react';
|
||||
import { FaPlug } from 'react-icons/fa';
|
||||
|
||||
import { DeckyState, DeckyStateContextProvider, useDeckyState } from './components/DeckyState';
|
||||
import LegacyPlugin from './components/LegacyPlugin';
|
||||
import { deinitFilepickerPatches, initFilepickerPatches } from './components/modals/filepicker/patches';
|
||||
import PluginInstallModal from './components/modals/PluginInstallModal';
|
||||
import NotificationBadge from './components/NotificationBadge';
|
||||
import PluginView from './components/PluginView';
|
||||
import TitleView from './components/TitleView';
|
||||
import WithSuspense from './components/WithSuspense';
|
||||
import Logger from './logger';
|
||||
import { Plugin } from './plugin';
|
||||
import RouterHook from './router-hook';
|
||||
@@ -16,6 +18,11 @@ import TabsHook from './tabs-hook';
|
||||
import Toaster from './toaster';
|
||||
import { VerInfo, callUpdaterMethod } from './updater';
|
||||
|
||||
const StorePage = lazy(() => import('./components/store/Store'));
|
||||
const SettingsPage = lazy(() => import('./components/settings'));
|
||||
|
||||
const FilePicker = lazy(() => import('./components/modals/filepicker'));
|
||||
|
||||
declare global {
|
||||
interface Window {}
|
||||
}
|
||||
@@ -36,7 +43,7 @@ class PluginLoader extends Logger {
|
||||
super(PluginLoader.name);
|
||||
this.log('Initialized');
|
||||
|
||||
const TabIcon = () => {
|
||||
const TabBadge = () => {
|
||||
const { updates, hasLoaderUpdate } = useDeckyState();
|
||||
return <NotificationBadge show={(updates && updates.size > 0) || hasLoaderUpdate} />;
|
||||
};
|
||||
@@ -53,57 +60,40 @@ class PluginLoader extends Logger {
|
||||
icon: (
|
||||
<DeckyStateContextProvider deckyState={this.deckyState}>
|
||||
<FaPlug />
|
||||
<TabIcon />
|
||||
<TabBadge />
|
||||
</DeckyStateContextProvider>
|
||||
),
|
||||
});
|
||||
|
||||
const StorePage = lazy(() => import('./components/store/Store'));
|
||||
const SettingsPage = lazy(() => import('./components/settings'));
|
||||
|
||||
this.routerHook.addRoute('/decky/store', () => (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div
|
||||
style={{
|
||||
marginTop: '40px',
|
||||
height: 'calc( 100% - 40px )',
|
||||
overflowY: 'scroll',
|
||||
}}
|
||||
>
|
||||
<SteamSpinner />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<WithSuspense route={true}>
|
||||
<StorePage />
|
||||
</Suspense>
|
||||
</WithSuspense>
|
||||
));
|
||||
this.routerHook.addRoute('/decky/settings', () => {
|
||||
return (
|
||||
<DeckyStateContextProvider deckyState={this.deckyState}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div
|
||||
style={{
|
||||
marginTop: '40px',
|
||||
height: 'calc( 100% - 40px )',
|
||||
overflowY: 'scroll',
|
||||
}}
|
||||
>
|
||||
<SteamSpinner />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<WithSuspense route={true}>
|
||||
<SettingsPage />
|
||||
</Suspense>
|
||||
</WithSuspense>
|
||||
</DeckyStateContextProvider>
|
||||
);
|
||||
});
|
||||
|
||||
initFilepickerPatches();
|
||||
|
||||
this.updateVersion();
|
||||
}
|
||||
|
||||
public async updateVersion() {
|
||||
const versionInfo = (await callUpdaterMethod('get_version')).result as VerInfo;
|
||||
this.deckyState.setVersionInfo(versionInfo);
|
||||
|
||||
return versionInfo;
|
||||
}
|
||||
|
||||
public async notifyUpdates() {
|
||||
const versionInfo = (await callUpdaterMethod('get_version')).result as VerInfo;
|
||||
this.deckyState.setVersionInfo(versionInfo);
|
||||
const versionInfo = await this.updateVersion();
|
||||
if (versionInfo?.remote && versionInfo?.remote?.tag_name != versionInfo?.current) {
|
||||
this.toaster.toast({
|
||||
title: 'Decky',
|
||||
@@ -147,7 +137,7 @@ class PluginLoader extends Logger {
|
||||
|
||||
public uninstallPlugin(name: string) {
|
||||
showModal(
|
||||
<ModalRoot
|
||||
<ConfirmModal
|
||||
onOK={async () => {
|
||||
await this.callServerMethod('uninstall_plugin', { name });
|
||||
}}
|
||||
@@ -158,7 +148,7 @@ class PluginLoader extends Logger {
|
||||
<div className={staticClasses.Title} style={{ flexDirection: 'column' }}>
|
||||
Uninstall {name}?
|
||||
</div>
|
||||
</ModalRoot>,
|
||||
</ConfirmModal>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -176,6 +166,7 @@ class PluginLoader extends Logger {
|
||||
public deinit() {
|
||||
this.routerHook.removeRoute('/decky/store');
|
||||
this.routerHook.removeRoute('/decky/settings');
|
||||
deinitFilepickerPatches();
|
||||
}
|
||||
|
||||
public unloadPlugin(name: string) {
|
||||
@@ -225,7 +216,8 @@ class PluginLoader extends Logger {
|
||||
},
|
||||
});
|
||||
if (res.ok) {
|
||||
let plugin = await eval(await res.text())(this.createPluginAPI(name));
|
||||
let plugin_export = await eval(await res.text());
|
||||
let plugin = plugin_export(this.createPluginAPI(name));
|
||||
this.plugins.push({
|
||||
...plugin,
|
||||
name: name,
|
||||
@@ -257,11 +249,41 @@ class PluginLoader extends Logger {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
openFilePicker(
|
||||
startPath: string,
|
||||
includeFiles?: boolean,
|
||||
regex?: RegExp,
|
||||
): Promise<{ path: string; realpath: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const Content = ({ closeModal }: { closeModal?: () => void }) => (
|
||||
// Purposely outside of the FilePicker component as lazy-loaded ModalRoots don't focus correctly
|
||||
<ModalRoot
|
||||
onCancel={() => {
|
||||
reject('User canceled');
|
||||
closeModal?.();
|
||||
}}
|
||||
>
|
||||
<WithSuspense>
|
||||
<FilePicker
|
||||
startPath={startPath}
|
||||
includeFiles={includeFiles}
|
||||
regex={regex}
|
||||
onSubmit={resolve}
|
||||
closeModal={closeModal}
|
||||
/>
|
||||
</WithSuspense>
|
||||
</ModalRoot>
|
||||
);
|
||||
showModal(<Content />);
|
||||
});
|
||||
}
|
||||
|
||||
createPluginAPI(pluginName: string) {
|
||||
return {
|
||||
routerHook: this.routerHook,
|
||||
toaster: this.toaster,
|
||||
callServerMethod: this.callServerMethod,
|
||||
openFilePicker: this.openFilePicker,
|
||||
async callPluginMethod(methodName: string, args = {}) {
|
||||
const response = await fetch(`http://127.0.0.1:1337/plugins/${pluginName}/methods/${methodName}`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ModalRoot, showModal, staticClasses } from 'decky-frontend-lib';
|
||||
import { ConfirmModal, showModal, staticClasses } from 'decky-frontend-lib';
|
||||
|
||||
import { Plugin } from './plugin';
|
||||
|
||||
@@ -51,7 +51,7 @@ export async function installFromURL(url: string) {
|
||||
|
||||
export function requestLegacyPluginInstall(plugin: LegacyStorePlugin, selectedVer: string) {
|
||||
showModal(
|
||||
<ModalRoot
|
||||
<ConfirmModal
|
||||
onOK={() => {
|
||||
window.DeckyPluginLoader.callServerMethod('install_plugin', {
|
||||
name: plugin.artifact,
|
||||
@@ -70,7 +70,7 @@ export function requestLegacyPluginInstall(plugin: LegacyStorePlugin, selectedVe
|
||||
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>,
|
||||
</ConfirmModal>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,7 @@ class TabsHook extends Logger {
|
||||
});
|
||||
this.cNode = scrollRoot;
|
||||
this.cNode.stateNode.forceUpdate();
|
||||
this.log('Finished initial injection');
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
import { Patch, ToastData, afterPatch, findInReactTree, findModuleChild, sleep } from 'decky-frontend-lib';
|
||||
import {
|
||||
Patch,
|
||||
ToastData,
|
||||
afterPatch,
|
||||
callOriginal,
|
||||
findInReactTree,
|
||||
findModuleChild,
|
||||
replacePatch,
|
||||
sleep,
|
||||
staticClasses,
|
||||
} from 'decky-frontend-lib';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import Toast from './components/Toast';
|
||||
@@ -15,6 +25,7 @@ class Toaster extends Logger {
|
||||
private instanceRetPatch?: Patch;
|
||||
private node: any;
|
||||
private settingsModule: any;
|
||||
private ready: boolean = false;
|
||||
|
||||
constructor() {
|
||||
super('Toaster');
|
||||
@@ -25,14 +36,25 @@ class Toaster extends Logger {
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.settingsModule = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
let instance: any;
|
||||
const self = this;
|
||||
const focusManager = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return false;
|
||||
for (let prop in m) {
|
||||
if (typeof m[prop]?.settings?.bDisableToastsInGame !== 'undefined') return m[prop];
|
||||
if (m[prop]?.prototype?.TakeFocus) return m[prop];
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
let instance: any;
|
||||
const focusWorkaroundPatch = replacePatch(focusManager.prototype, 'BFocusWithin', function () {
|
||||
// @ts-ignore
|
||||
if (this.m_node?.m_element && this.m_node?.m_element.classList.contains(staticClasses.TabGroupPanel)) {
|
||||
self.debug('Intercepted friends re-focus');
|
||||
return true;
|
||||
}
|
||||
|
||||
return callOriginal;
|
||||
});
|
||||
while (true) {
|
||||
instance = findInReactTree(
|
||||
(document.getElementById('root') as any)._reactRootContainer._internalRoot.current,
|
||||
@@ -71,11 +93,22 @@ class Toaster extends Logger {
|
||||
return ret;
|
||||
};
|
||||
this.node.stateNode.forceUpdate();
|
||||
this.settingsModule = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (typeof m[prop]?.settings && m[prop]?.communityPreferences) return m[prop];
|
||||
}
|
||||
});
|
||||
focusWorkaroundPatch.unpatch();
|
||||
this.log('Initialized');
|
||||
this.ready = true;
|
||||
}
|
||||
|
||||
toast(toast: ToastData) {
|
||||
const settings = this.settingsModule.settings;
|
||||
async toast(toast: ToastData) {
|
||||
while (!this.ready) {
|
||||
await sleep(100);
|
||||
}
|
||||
const settings = this.settingsModule?.settings;
|
||||
let toastData = {
|
||||
nNotificationID: window.NotificationStore.m_nNextTestNotificationID++,
|
||||
rtCreated: Date.now(),
|
||||
@@ -87,8 +120,8 @@ class Toaster extends Logger {
|
||||
// @ts-ignore
|
||||
toastData.data.appid = () => 0;
|
||||
if (
|
||||
(settings.bDisableAllToasts && !toast.critical) ||
|
||||
(settings.bDisableToastsInGame && !toast.critical && window.NotificationStore.BIsUserInGame())
|
||||
(settings?.bDisableAllToasts && !toast.critical) ||
|
||||
(settings?.bDisableToastsInGame && !toast.critical && window.NotificationStore.BIsUserInGame())
|
||||
)
|
||||
return;
|
||||
window.NotificationStore.m_rgNotificationToasts.push(toastData);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { sleep } from 'decky-frontend-lib';
|
||||
|
||||
export enum Branches {
|
||||
Release,
|
||||
Prerelease,
|
||||
@@ -46,6 +44,4 @@ export async function callUpdaterMethod(methodName: string, args = {}) {
|
||||
|
||||
export async function finishUpdate() {
|
||||
callUpdaterMethod('do_restart');
|
||||
await sleep(3000);
|
||||
location.reload();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user