Compare commits

..

23 Commits

Author SHA1 Message Date
AAGaming 3700dd7437 fix plugin updating 2022-09-18 15:44:16 -04:00
AAGaming c6d48389c9 bump lib 2022-09-18 15:44:01 -04:00
AAGaming 490fc18008 enable debug logs in dev 2022-09-18 15:43:26 -04:00
AAGaming 797c7ea3b0 hopefully actually fix toaster 2022-09-18 15:03:59 -04:00
AAGaming 0f06bc1ef0 Updater UI tweaks, Markdown tweaks, bump lib 2022-09-18 14:53:36 -04:00
AAGaming c774451ff4 don't crash when plugin backends do 2022-09-18 14:51:17 -04:00
AAGaming 62a5bdbbb0 add more URLs to bypass authentication 2022-09-18 14:49:56 -04:00
AAGaming 7716c73014 fix plugin loading after install, move updater reloads to loader 2022-09-18 14:49:32 -04:00
AAGaming 8829adc5b6 update tasks 2022-09-18 14:48:08 -04:00
AAGaming 62bd3e76bd small changes making breakpoint debugging easier 2022-09-18 11:07:30 -04:00
AAGaming 9867d7bea0 Possibly actually fix the toasts issue 2022-09-18 10:17:39 -04:00
AAGaming c4d6731401 fix updater for new installs, fix file picker patch, fix scrolling on patch notes, fix tasks dir 2022-09-17 23:23:51 -04:00
AAGaming fded2fa8bf fix plugin install modal 2022-09-17 16:02:03 -04:00
TrainDoctor 90c523ec45 Update updater.py 2022-09-17 09:26:24 -07:00
AAGaming c5ccb4dfb8 fix TS errors in Toast 2022-09-17 11:36:53 -04:00
AAGaming 8b1925bc53 fix spacing [skip ci] 2022-09-17 11:35:31 -04:00
AAGaming a8c7c2f18f fix asyncio error on get_branch 2022-09-17 11:35:05 -04:00
AAGaming 463258febb wait for toaster ready, hopefully fix file browser patch 2022-09-16 18:49:35 -04:00
TrainDoctor 304fc0f94c Update build.yml 2022-09-11 15:40:19 -07:00
AAGaming b5b041fdee add file picker, add library file picker patch, bump lib, logger tweaks 2022-09-09 16:25:52 -04:00
AAGaming 9d980618a7 spice up js-side logs from store and branch select 2022-09-08 20:19:54 -04:00
Nik 6dad3f81e8 Updated screenshot 2022-09-08 23:22:47 +02:00
TrainDoctor adc1a792fb Whoopsie 2022-09-05 20:13:08 -07:00
30 changed files with 1018 additions and 172 deletions
+3 -3
View File
@@ -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
+21 -15
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
# Plugin Loader [![Chat](https://img.shields.io/badge/chat-on%20discord-7289da.svg)](https://discord.gg/ZU74G2NJzk)
![steamuserimages-a akamaihd](https://user-images.githubusercontent.com/10835354/161068262-ca723dc5-6795-417a-80f6-d8c1f9d03e93.jpg)
![Decky](https://media.discordapp.net/attachments/966017112244125756/1012466063893610506/main.jpg)
Keep an eye on the [Wiki](https://deckbrew.xyz) for more information about Plugin Loader, documentation + tools for plugin development and more.
+21 -5
View File
@@ -3,7 +3,7 @@ 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
@@ -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,8 +40,9 @@ 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
@@ -58,6 +60,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 +78,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:
@@ -93,9 +101,17 @@ class PluginBrowser:
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)
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:
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()}")
+1 -1
View File
@@ -27,7 +27,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')
+6 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+4 -1
View File
@@ -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"
+263 -6
View File
@@ -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
View File
@@ -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({
+35 -3
View File
@@ -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;
+2 -2
View File
@@ -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: {
+38
View File
@@ -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,7 +14,7 @@ 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 () => {
@@ -34,7 +34,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'}
+5 -2
View File
@@ -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);
})();
}, []);
+5 -1
View File
@@ -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;
+62 -40
View File
@@ -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',
+3 -3
View File
@@ -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>,
);
}
+15 -11
View File
@@ -15,6 +15,7 @@ class Toaster extends Logger {
private instanceRetPatch?: Patch;
private node: any;
private settingsModule: any;
private ready: boolean = false;
constructor() {
super('Toaster');
@@ -25,13 +26,6 @@ class Toaster extends Logger {
}
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(
@@ -71,11 +65,21 @@ 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];
}
});
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 +91,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);
-4
View File
@@ -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();
}