mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-07-12 16:21:54 +00:00
more major websocket progress
This commit is contained in:
@@ -4,7 +4,7 @@ import json
|
|||||||
# from pprint import pformat
|
# from pprint import pformat
|
||||||
|
|
||||||
# Partial imports
|
# Partial imports
|
||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession, request
|
||||||
from asyncio import sleep
|
from asyncio import sleep
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
@@ -123,7 +123,6 @@ class PluginBrowser:
|
|||||||
async def uninstall_plugin(self, name: str):
|
async def uninstall_plugin(self, name: str):
|
||||||
if self.loader.watcher:
|
if self.loader.watcher:
|
||||||
self.loader.watcher.disabled = True
|
self.loader.watcher.disabled = True
|
||||||
tab = await get_gamepadui_tab()
|
|
||||||
plugin_folder = self.find_plugin_folder(name)
|
plugin_folder = self.find_plugin_folder(name)
|
||||||
assert plugin_folder is not None
|
assert plugin_folder is not None
|
||||||
plugin_dir = path.join(self.plugin_path, plugin_folder)
|
plugin_dir = path.join(self.plugin_path, plugin_folder)
|
||||||
@@ -131,8 +130,7 @@ class PluginBrowser:
|
|||||||
logger.info("uninstalling " + name)
|
logger.info("uninstalling " + name)
|
||||||
logger.info(" at dir " + plugin_dir)
|
logger.info(" at dir " + plugin_dir)
|
||||||
logger.debug("calling frontend unload for %s" % str(name))
|
logger.debug("calling frontend unload for %s" % str(name))
|
||||||
res = await tab.evaluate_js(f"DeckyPluginLoader.unloadPlugin('{name}')")
|
await self.loader.ws.emit("loader/unload_plugin", name)
|
||||||
logger.debug("result of unload from UI: %s", res)
|
|
||||||
# plugins_snapshot = self.plugins.copy()
|
# plugins_snapshot = self.plugins.copy()
|
||||||
# snapshot_string = pformat(plugins_snapshot)
|
# snapshot_string = pformat(plugins_snapshot)
|
||||||
# logger.debug("current plugins: %s", snapshot_string)
|
# logger.debug("current plugins: %s", snapshot_string)
|
||||||
@@ -258,20 +256,14 @@ class PluginBrowser:
|
|||||||
async def request_plugin_install(self, artifact: str, name: str, version: str, hash: str, install_type: PluginInstallType):
|
async def request_plugin_install(self, artifact: str, name: str, version: str, hash: str, install_type: PluginInstallType):
|
||||||
request_id = str(time())
|
request_id = str(time())
|
||||||
self.install_requests[request_id] = PluginInstallContext(artifact, name, version, hash)
|
self.install_requests[request_id] = PluginInstallContext(artifact, name, version, hash)
|
||||||
tab = await get_gamepadui_tab()
|
|
||||||
await tab.open_websocket()
|
await self.loader.ws.emit("loader/add_plugin_install_prompt", name, version, request_id, hash, install_type)
|
||||||
await tab.evaluate_js(f"DeckyPluginLoader.addPluginInstallPrompt('{name}', '{version}', '{request_id}', '{hash}', {install_type})")
|
|
||||||
|
|
||||||
async def request_multiple_plugin_installs(self, requests: List[PluginInstallRequest]):
|
async def request_multiple_plugin_installs(self, requests: List[PluginInstallRequest]):
|
||||||
request_id = str(time())
|
request_id = str(time())
|
||||||
self.install_requests[request_id] = [PluginInstallContext(req['artifact'], req['name'], req['version'], req['hash']) for req in requests]
|
self.install_requests[request_id] = [PluginInstallContext(req['artifact'], req['name'], req['version'], req['hash']) for req in requests]
|
||||||
js_requests_parameter = ','.join([
|
|
||||||
f"{{ name: '{req['name']}', version: '{req['version']}', hash: '{req['hash']}', install_type: {req['install_type']}}}" for req in requests
|
|
||||||
])
|
|
||||||
|
|
||||||
tab = await get_gamepadui_tab()
|
await self.loader.ws.emit("loader/add_multiple_plugins_install_prompt", request_id, requests)
|
||||||
await tab.open_websocket()
|
|
||||||
await tab.evaluate_js(f"DeckyPluginLoader.addMultiplePluginsInstallPrompt('{request_id}', [{js_requests_parameter}])")
|
|
||||||
|
|
||||||
async def confirm_plugin_install(self, request_id: str):
|
async def confirm_plugin_install(self, request_id: str):
|
||||||
requestOrRequests = self.install_requests.pop(request_id)
|
requestOrRequests = self.install_requests.pop(request_id)
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
class UserType(Enum):
|
|
||||||
HOST_USER = 1
|
|
||||||
EFFECTIVE_USER = 2
|
|
||||||
ROOT = 3
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
|
class UserType(IntEnum):
|
||||||
|
HOST_USER = 1
|
||||||
|
EFFECTIVE_USER = 2
|
||||||
|
ROOT = 3
|
||||||
|
|
||||||
|
class PluginLoadType(IntEnum):
|
||||||
|
LEGACY_EVAL_IIFE = 0 # legacy, uses legacy serverAPI
|
||||||
|
ESMODULE_V1 = 1 # esmodule loading with modern @decky/backend apis
|
||||||
@@ -12,7 +12,7 @@ from aiohttp.web import Request, Response, middleware
|
|||||||
from aiohttp.typedefs import Handler
|
from aiohttp.typedefs import Handler
|
||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
from .localplatform import localplatform
|
from .localplatform import localplatform
|
||||||
from .customtypes import UserType
|
from .enums import UserType
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from packaging.version import Version
|
from packaging.version import Version
|
||||||
|
|
||||||
@@ -23,6 +23,7 @@ csrf_token = str(uuid.uuid4())
|
|||||||
ssl_ctx = ssl.create_default_context(cafile=certifi.where())
|
ssl_ctx = ssl.create_default_context(cafile=certifi.where())
|
||||||
|
|
||||||
assets_regex = re.compile("^/plugins/.*/assets/.*")
|
assets_regex = re.compile("^/plugins/.*/assets/.*")
|
||||||
|
dist_regex = re.compile("^/plugins/.*/dist/.*")
|
||||||
frontend_regex = re.compile("^/frontend/.*")
|
frontend_regex = re.compile("^/frontend/.*")
|
||||||
logger = getLogger("Main")
|
logger = getLogger("Main")
|
||||||
|
|
||||||
@@ -34,7 +35,18 @@ def get_csrf_token():
|
|||||||
|
|
||||||
@middleware
|
@middleware
|
||||||
async def csrf_middleware(request: Request, handler: Handler):
|
async def csrf_middleware(request: Request, handler: 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("/steam_resource/") or str(request.rel_url).startswith("/frontend/") or str(request.rel_url.path) == "/ws" 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("/steam_resource/") or \
|
||||||
|
str(request.rel_url).startswith("/frontend/") or \
|
||||||
|
str(request.rel_url.path) == "/ws" or \
|
||||||
|
assets_regex.match(str(request.rel_url)) or \
|
||||||
|
dist_regex.match(str(request.rel_url)) or \
|
||||||
|
frontend_regex.match(str(request.rel_url)):
|
||||||
|
|
||||||
return await handler(request)
|
return await handler(request)
|
||||||
return Response(text='Forbidden', status=403)
|
return Response(text='Forbidden', status=403)
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ from typing import TYPE_CHECKING, List
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .main import PluginManager
|
from .main import PluginManager
|
||||||
|
|
||||||
from .injector import get_gamepadui_tab
|
|
||||||
from .plugin.plugin import PluginWrapper
|
from .plugin.plugin import PluginWrapper
|
||||||
from .wsrouter import WSRouter
|
from .wsrouter import WSRouter
|
||||||
|
from .enums import PluginLoadType
|
||||||
|
|
||||||
Plugins = dict[str, PluginWrapper]
|
Plugins = dict[str, PluginWrapper]
|
||||||
ReloadQueue = Queue[Tuple[str, str, bool | None] | Tuple[str, str]]
|
ReloadQueue = Queue[Tuple[str, str, bool | None] | Tuple[str, str]]
|
||||||
@@ -96,6 +96,7 @@ class Loader:
|
|||||||
web.get("/frontend/{path:.*}", self.handle_frontend_assets),
|
web.get("/frontend/{path:.*}", self.handle_frontend_assets),
|
||||||
web.get("/locales/{path:.*}", self.handle_frontend_locales),
|
web.get("/locales/{path:.*}", self.handle_frontend_locales),
|
||||||
web.get("/plugins/{plugin_name}/frontend_bundle", self.handle_frontend_bundle),
|
web.get("/plugins/{plugin_name}/frontend_bundle", self.handle_frontend_bundle),
|
||||||
|
web.get("/plugins/{plugin_name}/dist/{path:.*}", self.handle_plugin_dist),
|
||||||
web.get("/plugins/{plugin_name}/assets/{path:.*}", self.handle_plugin_frontend_assets),
|
web.get("/plugins/{plugin_name}/assets/{path:.*}", self.handle_plugin_frontend_assets),
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -126,7 +127,13 @@ class Loader:
|
|||||||
|
|
||||||
async def get_plugins(self):
|
async def get_plugins(self):
|
||||||
plugins = list(self.plugins.values())
|
plugins = list(self.plugins.values())
|
||||||
return [{"name": str(i), "version": i.version} for i in plugins]
|
return [{"name": str(i), "version": i.version, "load_type": i.load_type} for i in plugins]
|
||||||
|
|
||||||
|
async def handle_plugin_dist(self, request: web.Request):
|
||||||
|
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||||
|
file = path.join(self.plugin_path, plugin.plugin_directory, "dist", request.match_info["path"])
|
||||||
|
|
||||||
|
return web.FileResponse(file, headers={"Cache-Control": "no-cache"})
|
||||||
|
|
||||||
async def handle_plugin_frontend_assets(self, request: web.Request):
|
async def handle_plugin_frontend_assets(self, request: web.Request):
|
||||||
plugin = self.plugins[request.match_info["plugin_name"]]
|
plugin = self.plugins[request.match_info["plugin_name"]]
|
||||||
@@ -145,7 +152,7 @@ class Loader:
|
|||||||
async def plugin_emitted_event(event: str, data: Any):
|
async def plugin_emitted_event(event: str, data: Any):
|
||||||
self.logger.debug(f"PLUGIN EMITTED EVENT: {str(event)} {data}")
|
self.logger.debug(f"PLUGIN EMITTED EVENT: {str(event)} {data}")
|
||||||
event_data = PluginEvent(plugin_name=plugin.name, event=event, data=data)
|
event_data = PluginEvent(plugin_name=plugin.name, event=event, data=data)
|
||||||
await self.ws.emit("plugin_event", event_data)
|
await self.ws.emit("loader/plugin_event", event_data)
|
||||||
|
|
||||||
plugin = PluginWrapper(file, plugin_directory, self.plugin_path, plugin_emitted_event)
|
plugin = PluginWrapper(file, plugin_directory, self.plugin_path, plugin_emitted_event)
|
||||||
if plugin.name in self.plugins:
|
if plugin.name in self.plugins:
|
||||||
@@ -166,9 +173,8 @@ class Loader:
|
|||||||
self.logger.error(f"Could not load {file}. {e}")
|
self.logger.error(f"Could not load {file}. {e}")
|
||||||
print_exc()
|
print_exc()
|
||||||
|
|
||||||
async def dispatch_plugin(self, name: str, version: str | None):
|
async def dispatch_plugin(self, name: str, version: str | None, load_type: int = PluginLoadType.ESMODULE_V1.value):
|
||||||
gpui_tab = await get_gamepadui_tab()
|
await self.ws.emit("loader/import_plugin", name, version, load_type)
|
||||||
await gpui_tab.evaluate_js(f"window.importDeckyPlugin('{name}', '{version}')")
|
|
||||||
|
|
||||||
def import_plugins(self):
|
def import_plugins(self):
|
||||||
self.logger.info(f"import plugins from {self.plugin_path}")
|
self.logger.info(f"import plugins from {self.plugin_path}")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import os, pwd, grp, sys, logging
|
import os, pwd, grp, sys, logging
|
||||||
from subprocess import call, run, DEVNULL, PIPE, STDOUT
|
from subprocess import call, run, DEVNULL, PIPE, STDOUT
|
||||||
from ..customtypes import UserType
|
from ..enums import UserType
|
||||||
|
|
||||||
logger = logging.getLogger("localplatform")
|
logger = logging.getLogger("localplatform")
|
||||||
|
|
||||||
@@ -157,6 +157,7 @@ async def service_start(service_name : str) -> bool:
|
|||||||
return res.returncode == 0
|
return res.returncode == 0
|
||||||
|
|
||||||
async def restart_webhelper() -> bool:
|
async def restart_webhelper() -> bool:
|
||||||
|
logger.info("Restarting steamwebhelper")
|
||||||
res = run(["killall", "-s", "SIGTERM", "steamwebhelper"], stdout=DEVNULL, stderr=DEVNULL)
|
res = run(["killall", "-s", "SIGTERM", "steamwebhelper"], stdout=DEVNULL, stderr=DEVNULL)
|
||||||
return res.returncode == 0
|
return res.returncode == 0
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from ..customtypes import UserType
|
from ..enums import UserType
|
||||||
import os, sys
|
import os, sys
|
||||||
|
|
||||||
def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool = True) -> bool:
|
def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool = True) -> bool:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ from .loader import Loader
|
|||||||
from .settings import SettingsManager
|
from .settings import SettingsManager
|
||||||
from .updater import Updater
|
from .updater import Updater
|
||||||
from .utilities import Utilities
|
from .utilities import Utilities
|
||||||
from .customtypes import UserType
|
from .enums import UserType
|
||||||
from .wsrouter import WSRouter
|
from .wsrouter import WSRouter
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ from logging import getLogger
|
|||||||
from os import path
|
from os import path
|
||||||
from multiprocessing import Process
|
from multiprocessing import Process
|
||||||
|
|
||||||
|
|
||||||
from .sandboxed_plugin import SandboxedPlugin
|
from .sandboxed_plugin import SandboxedPlugin
|
||||||
from .messages import MethodCallRequest, SocketMessageType
|
from .messages import MethodCallRequest, SocketMessageType
|
||||||
|
from ..enums import PluginLoadType
|
||||||
from ..localplatform.localsocket import LocalSocket
|
from ..localplatform.localsocket import LocalSocket
|
||||||
|
|
||||||
from typing import Any, Callable, Coroutine, Dict, List
|
from typing import Any, Callable, Coroutine, Dict, List
|
||||||
@@ -21,10 +21,14 @@ class PluginWrapper:
|
|||||||
|
|
||||||
self.version = None
|
self.version = None
|
||||||
|
|
||||||
|
self.load_type = PluginLoadType.LEGACY_EVAL_IIFE.value
|
||||||
|
|
||||||
json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r", encoding="utf-8"))
|
json = load(open(path.join(plugin_path, plugin_directory, "plugin.json"), "r", encoding="utf-8"))
|
||||||
if path.isfile(path.join(plugin_path, plugin_directory, "package.json")):
|
if path.isfile(path.join(plugin_path, plugin_directory, "package.json")):
|
||||||
package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r", encoding="utf-8"))
|
package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r", encoding="utf-8"))
|
||||||
self.version = package_json["version"]
|
self.version = package_json["version"]
|
||||||
|
if ("type" in package_json and package_json["type"] == "module"):
|
||||||
|
self.load_type = PluginLoadType.ESMODULE_V1.value
|
||||||
|
|
||||||
self.name = json["name"]
|
self.name = json["name"]
|
||||||
self.author = json["author"]
|
self.author = json["author"]
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from asyncio import (get_event_loop, new_event_loop,
|
|||||||
from .messages import SocketResponseDict, SocketMessageType
|
from .messages import SocketResponseDict, SocketMessageType
|
||||||
from ..localplatform.localsocket import LocalSocket
|
from ..localplatform.localsocket import LocalSocket
|
||||||
from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path
|
from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path
|
||||||
from ..customtypes import UserType
|
from ..enums import UserType
|
||||||
from .. import helpers
|
from .. import helpers
|
||||||
|
|
||||||
from typing import List, TypeVar, Type
|
from typing import List, TypeVar, Type
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from json import dump, load
|
|||||||
from os import mkdir, path, listdir, rename
|
from os import mkdir, path, listdir, rename
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
from .localplatform.localplatform import chown, folder_owner, get_chown_plugin_path
|
from .localplatform.localplatform import chown, folder_owner, get_chown_plugin_path
|
||||||
from .customtypes import UserType
|
from .enums import UserType
|
||||||
|
|
||||||
from .helpers import get_homebrew_path
|
from .helpers import get_homebrew_path
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ class TestingVersion(TypedDict):
|
|||||||
link: str
|
link: str
|
||||||
head_sha: str
|
head_sha: str
|
||||||
|
|
||||||
|
|
||||||
class Updater:
|
class Updater:
|
||||||
def __init__(self, context: PluginManager) -> None:
|
def __init__(self, context: PluginManager) -> None:
|
||||||
self.context = context
|
self.context = context
|
||||||
@@ -103,7 +102,7 @@ class Updater:
|
|||||||
logger.debug("checking for updates")
|
logger.debug("checking for updates")
|
||||||
selectedBranch = self.get_branch(self.context.settings)
|
selectedBranch = self.get_branch(self.context.settings)
|
||||||
async with ClientSession() as web:
|
async with ClientSession() as web:
|
||||||
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", ssl=helpers.get_ssl_context()) as res:
|
async with web.request("GET", "https://api.github.com/repos/SteamDeckHomebrew/decky-loader/releases", headers={'X-GitHub-Api-Version': '2022-11-28'}, ssl=helpers.get_ssl_context()) as res:
|
||||||
remoteVersions: List[RemoteVer] = await res.json()
|
remoteVersions: List[RemoteVer] = await res.json()
|
||||||
if selectedBranch == 0:
|
if selectedBranch == 0:
|
||||||
logger.debug("release type: release")
|
logger.debug("release type: release")
|
||||||
@@ -126,8 +125,7 @@ class Updater:
|
|||||||
logger.error("release type: NOT FOUND")
|
logger.error("release type: NOT FOUND")
|
||||||
raise ValueError("no valid branch found")
|
raise ValueError("no valid branch found")
|
||||||
logger.info("Updated remote version information")
|
logger.info("Updated remote version information")
|
||||||
tab = await get_gamepadui_tab()
|
await self.context.ws.emit("loader/notify_updates")
|
||||||
await tab.evaluate_js(f"window.DeckyPluginLoader.notifyUpdates()", False, True, False)
|
|
||||||
return await self.get_version_info()
|
return await self.get_version_info()
|
||||||
|
|
||||||
async def version_reloader(self):
|
async def version_reloader(self):
|
||||||
@@ -158,7 +156,7 @@ class Updater:
|
|||||||
raw += len(c)
|
raw += len(c)
|
||||||
new_progress = round((raw / total) * 100)
|
new_progress = round((raw / total) * 100)
|
||||||
if progress != new_progress:
|
if progress != new_progress:
|
||||||
self.context.loop.create_task(self.context.ws.emit("frontend/update_download_percentage", new_progress))
|
self.context.loop.create_task(self.context.ws.emit("updater/update_download_percentage", new_progress))
|
||||||
progress = new_progress
|
progress = new_progress
|
||||||
|
|
||||||
with open(path.join(getcwd(), ".loader.version"), "w", encoding="utf-8") as out:
|
with open(path.join(getcwd(), ".loader.version"), "w", encoding="utf-8") as out:
|
||||||
@@ -182,7 +180,7 @@ class Updater:
|
|||||||
logger.info(f"Setting the executable flag with chcon returned {await process.wait()}")
|
logger.info(f"Setting the executable flag with chcon returned {await process.wait()}")
|
||||||
|
|
||||||
logger.info("Updated loader installation.")
|
logger.info("Updated loader installation.")
|
||||||
await self.context.ws.emit("frontend/finish_download")
|
await self.context.ws.emit("updater/finish_download")
|
||||||
await self.do_restart()
|
await self.do_restart()
|
||||||
await tab.close_websocket()
|
await tab.close_websocket()
|
||||||
|
|
||||||
|
|||||||
@@ -93,9 +93,7 @@ class WSRouter:
|
|||||||
async for msg in ws:
|
async for msg in ws:
|
||||||
msg = cast(WSMessageExtra, msg)
|
msg = cast(WSMessageExtra, msg)
|
||||||
|
|
||||||
self.logger.debug(msg)
|
|
||||||
if msg.type == WSMsgType.TEXT:
|
if msg.type == WSMsgType.TEXT:
|
||||||
self.logger.debug(msg.data)
|
|
||||||
if msg.data == 'close':
|
if msg.data == 'close':
|
||||||
# TODO DO NOT RELY ON THIS!
|
# TODO DO NOT RELY ON THIS!
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -265,6 +265,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Testing": {
|
"Testing": {
|
||||||
"download": "Download"
|
"download": "Download",
|
||||||
|
"header": "The following versions of Decky Loader are built from open third-party Pull Requests. The Decky Loader team has not verified their functionality or security, and they may be outdated.",
|
||||||
|
"loading": "Loading open Pull Requests..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+5
-1
@@ -7,7 +7,7 @@ settings:
|
|||||||
dependencies:
|
dependencies:
|
||||||
decky-frontend-lib:
|
decky-frontend-lib:
|
||||||
specifier: 3.24.5
|
specifier: 3.24.5
|
||||||
version: link:../../lib
|
version: 3.24.5
|
||||||
filesize:
|
filesize:
|
||||||
specifier: ^10.0.7
|
specifier: ^10.0.7
|
||||||
version: 10.0.7
|
version: 10.0.7
|
||||||
@@ -1482,6 +1482,10 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.2
|
ms: 2.1.2
|
||||||
|
|
||||||
|
/decky-frontend-lib@3.24.5:
|
||||||
|
resolution: {integrity: sha512-eYlbKDOOcIBPI0b76Rqvlryq2ym/QNiry4xf2pFrXmBa1f95dflqbQAb2gTq9uHEa5gFmeV4lUcMPGJ3M14Xqw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/decode-named-character-reference@1.0.2:
|
/decode-named-character-reference@1.0.2:
|
||||||
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
|
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Patch, findModuleChild, replacePatch, sleep } from 'decky-frontend-lib';
|
import { Patch, findModuleChild, replacePatch, sleep } from 'decky-frontend-lib';
|
||||||
|
|
||||||
import Logger from '../../../../logger';
|
import Logger from '../../../../logger';
|
||||||
|
import { FileSelectionType } from '..';
|
||||||
|
|
||||||
const logger = new Logger('LibraryPatch');
|
const logger = new Logger('LibraryPatch');
|
||||||
|
|
||||||
@@ -13,7 +14,12 @@ function rePatch() {
|
|||||||
const details = window.appDetailsStore.GetAppDetails(appid);
|
const details = window.appDetailsStore.GetAppDetails(appid);
|
||||||
logger.debug('game details', details);
|
logger.debug('game details', details);
|
||||||
// strShortcutStartDir
|
// strShortcutStartDir
|
||||||
const file = await DeckyPluginLoader.openFilePicker(details?.strShortcutStartDir.replaceAll('"', '') || '/');
|
const file = await DeckyPluginLoader.openFilePicker(
|
||||||
|
FileSelectionType.FILE,
|
||||||
|
details?.strShortcutStartDir.replaceAll('"', '') || '/',
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
logger.debug('user selected', file);
|
logger.debug('user selected', file);
|
||||||
window.SteamClient.Apps.SetShortcutExe(appid, JSON.stringify(file.path));
|
window.SteamClient.Apps.SetShortcutExe(appid, JSON.stringify(file.path));
|
||||||
const pathArr = file.path.split('/');
|
const pathArr = file.path.split('/');
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const installFromZip = async () => {
|
|||||||
logger.error('The default path has not been found!');
|
logger.error('The default path has not been found!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
DeckyPluginLoader.openFilePickerV2(FileSelectionType.FILE, path, true, true, undefined, ['zip'], false, false).then(
|
DeckyPluginLoader.openFilePicker(FileSelectionType.FILE, path, true, true, undefined, ['zip'], false, false).then(
|
||||||
(val) => {
|
(val) => {
|
||||||
const url = `file://${val.path}`;
|
const url = `file://${val.path}`;
|
||||||
console.log(`Installing plugin locally from ${url}`);
|
console.log(`Installing plugin locally from ${url}`);
|
||||||
@@ -37,6 +37,8 @@ const installFromZip = async () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getTabID = DeckyBackend.callable<[name: string], string>('utilities/get_tab_id');
|
||||||
|
|
||||||
export default function DeveloperSettings() {
|
export default function DeveloperSettings() {
|
||||||
const [enableValveInternal, setEnableValveInternal] = useSetting<boolean>('developer.valve_internal', false);
|
const [enableValveInternal, setEnableValveInternal] = useSetting<boolean>('developer.valve_internal', false);
|
||||||
const [reactDevtoolsEnabled, setReactDevtoolsEnabled] = useSetting<boolean>('developer.rdt.enabled', false);
|
const [reactDevtoolsEnabled, setReactDevtoolsEnabled] = useSetting<boolean>('developer.rdt.enabled', false);
|
||||||
@@ -85,7 +87,7 @@ export default function DeveloperSettings() {
|
|||||||
<DialogButton
|
<DialogButton
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
let tabId = await DeckyBackend.call<[name: string], string>('utilities/get_tab_id', 'SharedJSContext');
|
let tabId = await getTabID('SharedJSContext');
|
||||||
Navigation.NavigateToExternalWeb(
|
Navigation.NavigateToExternalWeb(
|
||||||
'localhost:8080/devtools/inspector.html?ws=localhost:8080/devtools/page/' + tabId,
|
'localhost:8080/devtools/inspector.html?ws=localhost:8080/devtools/page/' + tabId,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -75,12 +75,12 @@ export default function UpdaterSettings() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const a = DeckyBackend.addEventListener('frontend/update_download_percentage', (percentage) => {
|
const a = DeckyBackend.addEventListener('updater/update_download_percentage', (percentage) => {
|
||||||
setUpdateProgress(percentage);
|
setUpdateProgress(percentage);
|
||||||
setIsLoaderUpdating(true);
|
setIsLoaderUpdating(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
const b = DeckyBackend.addEventListener('frontend/finish_download', () => {
|
const b = DeckyBackend.addEventListener('updater/finish_download', () => {
|
||||||
setUpdateProgress(0);
|
setUpdateProgress(0);
|
||||||
setReloading(true);
|
setReloading(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { DialogBody, DialogButton, DialogControlsSection, Focusable, Navigation } from 'decky-frontend-lib';
|
import {
|
||||||
|
DialogBody,
|
||||||
|
DialogButton,
|
||||||
|
DialogControlsSection,
|
||||||
|
Field,
|
||||||
|
Focusable,
|
||||||
|
Navigation,
|
||||||
|
SteamSpinner,
|
||||||
|
} from 'decky-frontend-lib';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FaDownload, FaInfo } from 'react-icons/fa';
|
import { FaDownload, FaInfo } from 'react-icons/fa';
|
||||||
@@ -19,13 +27,23 @@ const downloadTestingVersion = DeckyBackend.callable<[pr_id: number, sha: string
|
|||||||
export default function TestingVersionList() {
|
export default function TestingVersionList() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [testingVersions, setTestingVersions] = useState<TestingVersion[]>([]);
|
const [testingVersions, setTestingVersions] = useState<TestingVersion[]>([]);
|
||||||
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
setTestingVersions(await getTestingVersions());
|
setTestingVersions(await getTestingVersions());
|
||||||
|
setLoading(false);
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SteamSpinner>{t('Testing.loading')}</SteamSpinner>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (testingVersions.length === 0) {
|
if (testingVersions.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -37,13 +55,18 @@ export default function TestingVersionList() {
|
|||||||
return (
|
return (
|
||||||
<DialogBody>
|
<DialogBody>
|
||||||
<DialogControlsSection>
|
<DialogControlsSection>
|
||||||
|
<h4>{t('Testing.header')}</h4>
|
||||||
<ul style={{ listStyleType: 'none', padding: '0' }}>
|
<ul style={{ listStyleType: 'none', padding: '0' }}>
|
||||||
{testingVersions.map((version) => {
|
{testingVersions.map((version) => {
|
||||||
return (
|
return (
|
||||||
<li style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', paddingBottom: '10px' }}>
|
<li>
|
||||||
<span>
|
<Field
|
||||||
|
label={
|
||||||
|
<>
|
||||||
{version.name} <span style={{ opacity: '50%' }}>{'#' + version.id}</span>
|
{version.name} <span style={{ opacity: '50%' }}>{'#' + version.id}</span>
|
||||||
</span>
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Focusable style={{ height: '40px', marginLeft: 'auto', display: 'flex' }}>
|
<Focusable style={{ height: '40px', marginLeft: 'auto', display: 'flex' }}>
|
||||||
<DialogButton
|
<DialogButton
|
||||||
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
|
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
|
||||||
@@ -79,6 +102,7 @@ export default function TestingVersionList() {
|
|||||||
<FaInfo />
|
<FaInfo />
|
||||||
</DialogButton>
|
</DialogButton>
|
||||||
</Focusable>
|
</Focusable>
|
||||||
|
</Field>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }>
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const res = await getPluginList(selectedSort[0], selectedSort[1]);
|
const res = await getPluginList(selectedSort[0], selectedSort[1]);
|
||||||
logger.log('got data!', res);
|
logger.debug('got data!', res);
|
||||||
setPluginList(res);
|
setPluginList(res);
|
||||||
setPluginCount(res.length);
|
setPluginCount(res.length);
|
||||||
})();
|
})();
|
||||||
@@ -98,7 +98,7 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }>
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const storeRes = await getStore();
|
const storeRes = await getStore();
|
||||||
logger.log(`store is ${storeRes}, isTesting is ${storeRes === Store.Testing}`);
|
logger.debug(`store is ${storeRes}, isTesting is ${storeRes === Store.Testing}`);
|
||||||
setIsTesting(storeRes === Store.Testing);
|
setIsTesting(storeRes === Store.Testing);
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
+138
-53
@@ -2,7 +2,6 @@ import {
|
|||||||
ModalRoot,
|
ModalRoot,
|
||||||
PanelSection,
|
PanelSection,
|
||||||
PanelSectionRow,
|
PanelSectionRow,
|
||||||
Patch,
|
|
||||||
QuickAccessTab,
|
QuickAccessTab,
|
||||||
Router,
|
Router,
|
||||||
findSP,
|
findSP,
|
||||||
@@ -26,7 +25,7 @@ import { FrozenPluginService } from './frozen-plugins-service';
|
|||||||
import { HiddenPluginsService } from './hidden-plugins-service';
|
import { HiddenPluginsService } from './hidden-plugins-service';
|
||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
import { NotificationService } from './notification-service';
|
import { NotificationService } from './notification-service';
|
||||||
import { InstallType, Plugin } from './plugin';
|
import { InstallType, Plugin, PluginLoadType } from './plugin';
|
||||||
import RouterHook from './router-hook';
|
import RouterHook from './router-hook';
|
||||||
import { deinitSteamFixes, initSteamFixes } from './steamfixes';
|
import { deinitSteamFixes, initSteamFixes } from './steamfixes';
|
||||||
import { checkForPluginUpdates } from './store';
|
import { checkForPluginUpdates } from './store';
|
||||||
@@ -41,6 +40,18 @@ const SettingsPage = lazy(() => import('./components/settings'));
|
|||||||
|
|
||||||
const FilePicker = lazy(() => import('./components/modals/filepicker'));
|
const FilePicker = lazy(() => import('./components/modals/filepicker'));
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyPluginBackendAPIInit?: {
|
||||||
|
connect: (version: number, key: string) => any; // Returns the backend API used above, no real point adding types to this.
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const callPluginMethod = DeckyBackend.callable<[pluginName: string, method: string, ...args: any], any>(
|
||||||
|
'loader/call_plugin_method',
|
||||||
|
);
|
||||||
|
|
||||||
class PluginLoader extends Logger {
|
class PluginLoader extends Logger {
|
||||||
private plugins: Plugin[] = [];
|
private plugins: Plugin[] = [];
|
||||||
private tabsHook: TabsHook = new TabsHook();
|
private tabsHook: TabsHook = new TabsHook();
|
||||||
@@ -55,11 +66,21 @@ class PluginLoader extends Logger {
|
|||||||
private reloadLock: boolean = false;
|
private reloadLock: boolean = false;
|
||||||
// stores a list of plugin names which requested to be reloaded
|
// stores a list of plugin names which requested to be reloaded
|
||||||
private pluginReloadQueue: { name: string; version?: string }[] = [];
|
private pluginReloadQueue: { name: string; version?: string }[] = [];
|
||||||
|
private apiKeys: Map<string, string> = new Map();
|
||||||
private focusWorkaroundPatch?: Patch;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(PluginLoader.name);
|
super(PluginLoader.name);
|
||||||
|
console.log(import.meta.url);
|
||||||
|
|
||||||
|
DeckyBackend.addEventListener('loader/notify_updates', this.notifyUpdates.bind(this));
|
||||||
|
DeckyBackend.addEventListener('loader/import_plugin', this.importPlugin.bind(this));
|
||||||
|
DeckyBackend.addEventListener('loader/unload_plugin', this.unloadPlugin.bind(this));
|
||||||
|
DeckyBackend.addEventListener('loader/add_plugin_install_prompt', this.addPluginInstallPrompt.bind(this));
|
||||||
|
DeckyBackend.addEventListener(
|
||||||
|
'loader/add_multiple_plugins_install_prompt',
|
||||||
|
this.addMultiplePluginsInstallPrompt.bind(this),
|
||||||
|
);
|
||||||
|
|
||||||
this.tabsHook.init();
|
this.tabsHook.init();
|
||||||
|
|
||||||
const TabBadge = () => {
|
const TabBadge = () => {
|
||||||
@@ -108,7 +129,10 @@ class PluginLoader extends Logger {
|
|||||||
.then(() => this.log('Initialized'));
|
.then(() => this.log('Initialized'));
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPluginsFromBackend = DeckyBackend.callable<[], { name: string; version: string }[]>('loader/get_plugins');
|
private getPluginsFromBackend = DeckyBackend.callable<
|
||||||
|
[],
|
||||||
|
{ name: string; version: string; load_type: PluginLoadType }[]
|
||||||
|
>('loader/get_plugins');
|
||||||
|
|
||||||
private async loadPlugins() {
|
private async loadPlugins() {
|
||||||
// wait for SP window to exist before loading plugins
|
// wait for SP window to exist before loading plugins
|
||||||
@@ -119,7 +143,8 @@ class PluginLoader extends Logger {
|
|||||||
const pluginLoadPromises = [];
|
const pluginLoadPromises = [];
|
||||||
const loadStart = performance.now();
|
const loadStart = performance.now();
|
||||||
for (const plugin of plugins) {
|
for (const plugin of plugins) {
|
||||||
if (!this.hasPlugin(plugin.name)) pluginLoadPromises.push(this.importPlugin(plugin.name, plugin.version, false));
|
if (!this.hasPlugin(plugin.name))
|
||||||
|
pluginLoadPromises.push(this.importPlugin(plugin.name, plugin.version, plugin.load_type, false));
|
||||||
}
|
}
|
||||||
await Promise.all(pluginLoadPromises);
|
await Promise.all(pluginLoadPromises);
|
||||||
const loadEnd = performance.now();
|
const loadEnd = performance.now();
|
||||||
@@ -256,7 +281,6 @@ class PluginLoader extends Logger {
|
|||||||
this.routerHook.removeRoute('/decky/settings');
|
this.routerHook.removeRoute('/decky/settings');
|
||||||
deinitSteamFixes();
|
deinitSteamFixes();
|
||||||
deinitFilepickerPatches();
|
deinitFilepickerPatches();
|
||||||
this.focusWorkaroundPatch?.unpatch();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public unloadPlugin(name: string) {
|
public unloadPlugin(name: string) {
|
||||||
@@ -266,7 +290,12 @@ class PluginLoader extends Logger {
|
|||||||
this.deckyState.setPlugins(this.plugins);
|
this.deckyState.setPlugins(this.plugins);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async importPlugin(name: string, version?: string | undefined, useQueue: boolean = true) {
|
public async importPlugin(
|
||||||
|
name: string,
|
||||||
|
version?: string | undefined,
|
||||||
|
loadType: PluginLoadType = PluginLoadType.ESMODULE_V1,
|
||||||
|
useQueue: boolean = true,
|
||||||
|
) {
|
||||||
if (useQueue && this.reloadLock) {
|
if (useQueue && this.reloadLock) {
|
||||||
this.log('Reload currently in progress, adding to queue', name);
|
this.log('Reload currently in progress, adding to queue', name);
|
||||||
this.pluginReloadQueue.push({ name, version: version });
|
this.pluginReloadQueue.push({ name, version: version });
|
||||||
@@ -279,7 +308,7 @@ class PluginLoader extends Logger {
|
|||||||
|
|
||||||
this.unloadPlugin(name);
|
this.unloadPlugin(name);
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
await this.importReactPlugin(name, version);
|
await this.importReactPlugin(name, version, loadType);
|
||||||
const endTime = performance.now();
|
const endTime = performance.now();
|
||||||
|
|
||||||
this.deckyState.setPlugins(this.plugins);
|
this.deckyState.setPlugins(this.plugins);
|
||||||
@@ -297,32 +326,57 @@ class PluginLoader extends Logger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async importReactPlugin(name: string, version?: string) {
|
private async importReactPlugin(
|
||||||
|
name: string,
|
||||||
|
version?: string,
|
||||||
|
loadType: PluginLoadType = PluginLoadType.ESMODULE_V1,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
switch (loadType) {
|
||||||
|
case PluginLoadType.ESMODULE_V1:
|
||||||
|
const uuid = this.initPluginBackendAPIConnection(name);
|
||||||
|
let plugin_export: () => Plugin;
|
||||||
|
try {
|
||||||
|
plugin_export = await import(`http://127.0.0.1:1337/plugins/${name}/dist/index.js#apiKey=${uuid}`);
|
||||||
|
} finally {
|
||||||
|
this.destroyPluginBackendAPIConnection(uuid);
|
||||||
|
}
|
||||||
|
let plugin = plugin_export();
|
||||||
|
|
||||||
|
this.plugins.push({
|
||||||
|
...plugin,
|
||||||
|
name: name,
|
||||||
|
version: version,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PluginLoadType.LEGACY_EVAL_IIFE:
|
||||||
let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, {
|
let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
Authentication: deckyAuthToken,
|
Authentication: deckyAuthToken,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
try {
|
let plugin_export: (serverAPI: any) => Plugin = await eval(await res.text());
|
||||||
let plugin_export = await eval(await res.text());
|
let plugin = plugin_export(this.createLegacyPluginAPI(name));
|
||||||
let plugin = plugin_export(this.createPluginAPI(name));
|
|
||||||
this.plugins.push({
|
this.plugins.push({
|
||||||
...plugin,
|
...plugin,
|
||||||
name: name,
|
name: name,
|
||||||
version: version,
|
version: version,
|
||||||
});
|
});
|
||||||
|
} else throw new Error(`${name} frontend_bundle not OK`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`${name} has no defined loadType.`);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.error('Error loading plugin ' + name, e);
|
this.error('Error loading plugin ' + name, e);
|
||||||
const TheError: FC<{}> = () => (
|
const TheError: FC<{}> = () => (
|
||||||
<PanelSection>
|
<PanelSection>
|
||||||
<PanelSectionRow>
|
<PanelSectionRow>
|
||||||
<div
|
<div className={quickAccessMenuClasses.FriendsTitle} style={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
className={quickAccessMenuClasses.FriendsTitle}
|
|
||||||
style={{ display: 'flex', justifyContent: 'center' }}
|
|
||||||
>
|
|
||||||
<TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="error" />
|
<TranslationHelper trans_class={TranslationClass.PLUGIN_LOADER} trans_text="error" />
|
||||||
</div>
|
</div>
|
||||||
</PanelSectionRow>
|
</PanelSectionRow>
|
||||||
@@ -360,7 +414,6 @@ class PluginLoader extends Logger {
|
|||||||
icon: <FaExclamationCircle />,
|
icon: <FaExclamationCircle />,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else throw new Error(`${name} frontend_bundle not OK`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async callServerMethod(methodName: string, args = {}) {
|
async callServerMethod(methodName: string, args = {}) {
|
||||||
@@ -374,20 +427,20 @@ class PluginLoader extends Logger {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
openFilePicker(
|
openFilePickerLegacy(
|
||||||
startPath: string,
|
startPath: string,
|
||||||
selectFiles?: boolean,
|
selectFiles?: boolean,
|
||||||
regex?: RegExp,
|
regex?: RegExp,
|
||||||
): Promise<{ path: string; realpath: string }> {
|
): Promise<{ path: string; realpath: string }> {
|
||||||
this.warn('openFilePicker is deprecated and will be removed. Please migrate to openFilePickerV2');
|
this.warn('openFilePicker is deprecated and will be removed. Please migrate to openFilePickerV2');
|
||||||
if (selectFiles) {
|
if (selectFiles) {
|
||||||
return this.openFilePickerV2(FileSelectionType.FILE, startPath, true, true, regex);
|
return this.openFilePicker(FileSelectionType.FILE, startPath, true, true, regex);
|
||||||
} else {
|
} else {
|
||||||
return this.openFilePickerV2(FileSelectionType.FOLDER, startPath, false, true, regex);
|
return this.openFilePicker(FileSelectionType.FOLDER, startPath, false, true, regex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
openFilePickerV2(
|
openFilePicker(
|
||||||
select: FileSelectionType,
|
select: FileSelectionType,
|
||||||
startPath: string,
|
startPath: string,
|
||||||
includeFiles?: boolean,
|
includeFiles?: boolean,
|
||||||
@@ -428,36 +481,6 @@ class PluginLoader extends Logger {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
createPluginAPI(pluginName: string) {
|
|
||||||
const pluginAPI = {
|
|
||||||
backend: {
|
|
||||||
call<Args extends any[] = any[], Return = void>(method: string, ...args: Args): Promise<Return> {
|
|
||||||
return DeckyBackend.call<[pluginName: string, method: string, ...args: Args], Return>(
|
|
||||||
'loader/call_plugin_method',
|
|
||||||
pluginName,
|
|
||||||
method,
|
|
||||||
...args,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
callable<Args extends any[] = any[], Return = void>(method: string): (...args: Args) => Promise<Return> {
|
|
||||||
return (...args) => pluginAPI.backend.call<Args, Return>(method, ...args);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
routerHook: this.routerHook,
|
|
||||||
toaster: this.toaster,
|
|
||||||
// Legacy
|
|
||||||
callServerMethod: this.callServerMethod,
|
|
||||||
openFilePicker: this.openFilePicker,
|
|
||||||
openFilePickerV2: this.openFilePickerV2,
|
|
||||||
// Legacy
|
|
||||||
async callPluginMethod(methodName: string, args = {}) {
|
|
||||||
return DeckyBackend.call<[pluginName: string, methodName: string, kwargs: any], any>(
|
|
||||||
'loader/call_legacy_plugin_method',
|
|
||||||
pluginName,
|
|
||||||
methodName,
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
/* TODO replace with the following flow (or similar) so we can reuse the JS Fetch API
|
/* TODO replace with the following flow (or similar) so we can reuse the JS Fetch API
|
||||||
frontend --request URL only--> backend (ws method)
|
frontend --request URL only--> backend (ws method)
|
||||||
backend --new temporary backend URL--> frontend (ws response)
|
backend --new temporary backend URL--> frontend (ws response)
|
||||||
@@ -483,7 +506,69 @@ class PluginLoader extends Logger {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { success: false, result: e?.toString() };
|
return { success: false, result: e?.toString() };
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyPluginBackendAPIConnection(uuid: string) {
|
||||||
|
if (this.apiKeys.delete(uuid)) {
|
||||||
|
this.debug(`backend api connection init data destroyed for ${uuid}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initPluginBackendAPI() {
|
||||||
|
// Things will break *very* badly if plugin code touches this outside of @decky/backend, so lets make that clear.
|
||||||
|
window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyPluginBackendAPIInit = {
|
||||||
|
connect: (version: number, key: string) => {
|
||||||
|
if (!this.apiKeys.has(key)) {
|
||||||
|
throw new Error(`Backend API key ${key} is invalid.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginName = this.apiKeys.get(key)!;
|
||||||
|
|
||||||
|
if (version <= 0) {
|
||||||
|
this.destroyPluginBackendAPIConnection(key);
|
||||||
|
throw new Error(`UUID ${key} requested invalid backend api version ${version}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const backendAPI = {
|
||||||
|
call: (methodName: string, ...args: any) => {
|
||||||
|
return callPluginMethod(pluginName, methodName, ...args);
|
||||||
},
|
},
|
||||||
|
callable: (methodName: string) => {
|
||||||
|
return (...args: any) => callPluginMethod(pluginName, methodName, ...args);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.destroyPluginBackendAPIConnection(key);
|
||||||
|
return backendAPI;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
initPluginBackendAPIConnection(pluginName: string) {
|
||||||
|
const key = crypto.randomUUID();
|
||||||
|
this.apiKeys.set(key, pluginName);
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
createLegacyPluginAPI(pluginName: string) {
|
||||||
|
const pluginAPI = {
|
||||||
|
routerHook: this.routerHook,
|
||||||
|
toaster: this.toaster,
|
||||||
|
// Legacy
|
||||||
|
callServerMethod: this.callServerMethod,
|
||||||
|
openFilePicker: this.openFilePickerLegacy,
|
||||||
|
openFilePickerV2: this.openFilePicker,
|
||||||
|
// Legacy
|
||||||
|
async callPluginMethod(methodName: string, args = {}) {
|
||||||
|
return DeckyBackend.call<[pluginName: string, methodName: string, kwargs: any], any>(
|
||||||
|
'loader/call_legacy_plugin_method',
|
||||||
|
pluginName,
|
||||||
|
methodName,
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
fetchNoCors: this.fetchNoCors,
|
||||||
executeInTab: DeckyBackend.callable<
|
executeInTab: DeckyBackend.callable<
|
||||||
[tab: String, runAsync: Boolean, code: string],
|
[tab: String, runAsync: Boolean, code: string],
|
||||||
{ success: boolean; result: any }
|
{ success: boolean; result: any }
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
export enum PluginLoadType {
|
||||||
|
LEGACY_EVAL_IIFE = 0, // legacy, uses legacy serverAPI
|
||||||
|
ESMODULE_V1 = 1, // esmodule loading with modern @decky/backend apis
|
||||||
|
}
|
||||||
|
|
||||||
export interface Plugin {
|
export interface Plugin {
|
||||||
name: string;
|
name: string;
|
||||||
version?: string;
|
version?: string;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import PluginLoader from './plugin-loader';
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
export var DeckyPluginLoader: PluginLoader;
|
export var DeckyPluginLoader: PluginLoader;
|
||||||
export var importDeckyPlugin: Function;
|
|
||||||
export var deckyHasLoaded: boolean;
|
export var deckyHasLoaded: boolean;
|
||||||
export var deckyHasConnectedRDT: boolean | undefined;
|
export var deckyHasConnectedRDT: boolean | undefined;
|
||||||
export var deckyAuthToken: string;
|
export var deckyAuthToken: string;
|
||||||
@@ -45,9 +44,7 @@ declare global {
|
|||||||
window?.DeckyPluginLoader?.deinit();
|
window?.DeckyPluginLoader?.deinit();
|
||||||
window.DeckyPluginLoader = new PluginLoader();
|
window.DeckyPluginLoader = new PluginLoader();
|
||||||
DeckyPluginLoader.init();
|
DeckyPluginLoader.init();
|
||||||
window.importDeckyPlugin = function (name: string, version: string) {
|
console.log(import.meta.url);
|
||||||
DeckyPluginLoader?.importPlugin(name, version);
|
|
||||||
};
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import reloadFix from './reload';
|
// import reloadFix from './reload';
|
||||||
import restartFix from './restart';
|
// import restartFix from './restart';
|
||||||
let fixes: Function[] = [];
|
let fixes: Function[] = [];
|
||||||
|
|
||||||
export function deinitSteamFixes() {
|
export function deinitSteamFixes() {
|
||||||
@@ -7,6 +7,6 @@ export function deinitSteamFixes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function initSteamFixes() {
|
export async function initSteamFixes() {
|
||||||
fixes.push(await reloadFix());
|
// fixes.push(await reloadFix());
|
||||||
fixes.push(await restartFix());
|
// fixes.push(await restartFix());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,6 @@ export const doRestart = DeckyBackend.callable('updater/do_restart');
|
|||||||
export const getVersionInfo = DeckyBackend.callable<[], VerInfo>('updater/get_version_info');
|
export const getVersionInfo = DeckyBackend.callable<[], VerInfo>('updater/get_version_info');
|
||||||
export const checkForUpdates = DeckyBackend.callable<[], VerInfo>('updater/check_for_updates');
|
export const checkForUpdates = DeckyBackend.callable<[], VerInfo>('updater/check_for_updates');
|
||||||
|
|
||||||
DeckyBackend.addEventListener('frontend/finish_download', async () => {
|
DeckyBackend.addEventListener('updater/finish_download', async () => {
|
||||||
await doRestart();
|
await doRestart();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ interface PromiseResolver<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class WSRouter extends Logger {
|
export class WSRouter extends Logger {
|
||||||
routes: Map<string, (...args: any) => any> = new Map();
|
|
||||||
runningCalls: Map<number, PromiseResolver<any>> = new Map();
|
runningCalls: Map<number, PromiseResolver<any>> = new Map();
|
||||||
eventListeners: Map<string, Set<(...args: any) => any>> = new Map();
|
eventListeners: Map<string, Set<(...args: any) => any>> = new Map();
|
||||||
ws?: WebSocket;
|
ws?: WebSocket;
|
||||||
@@ -92,14 +91,6 @@ export class WSRouter extends Logger {
|
|||||||
this.ws?.send(JSON.stringify(data));
|
this.ws?.send(JSON.stringify(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
addRoute(name: string, route: (...args: any) => any) {
|
|
||||||
this.routes.set(name, route);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeRoute(name: string) {
|
|
||||||
this.routes.delete(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
addEventListener(event: string, listener: (...args: any) => any) {
|
addEventListener(event: string, listener: (...args: any) => any) {
|
||||||
if (!this.eventListeners.has(event)) {
|
if (!this.eventListeners.has(event)) {
|
||||||
this.eventListeners.set(event, new Set([listener]));
|
this.eventListeners.set(event, new Set([listener]));
|
||||||
@@ -123,20 +114,6 @@ export class WSRouter extends Logger {
|
|||||||
try {
|
try {
|
||||||
const data = JSON.parse(msg.data) as Message;
|
const data = JSON.parse(msg.data) as Message;
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case MessageType.CALL:
|
|
||||||
if (this.routes.has(data.route)) {
|
|
||||||
try {
|
|
||||||
const res = await this.routes.get(data.route)!(...data.args);
|
|
||||||
this.write({ type: MessageType.REPLY, id: data.id, result: res });
|
|
||||||
this.debug(`Started JS call ${data.route} ID ${data.id}`);
|
|
||||||
} catch (e) {
|
|
||||||
await this.write({ type: MessageType.ERROR, id: data.id, error: (e as Error)?.stack || e });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await this.write({ type: MessageType.ERROR, id: data.id, error: `Route ${data.route} does not exist.` });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MessageType.REPLY:
|
case MessageType.REPLY:
|
||||||
if (this.runningCalls.has(data.id)) {
|
if (this.runningCalls.has(data.id)) {
|
||||||
this.runningCalls.get(data.id)!.resolve(data.result);
|
this.runningCalls.get(data.id)!.resolve(data.result);
|
||||||
@@ -154,6 +131,7 @@ export class WSRouter extends Logger {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case MessageType.EVENT:
|
case MessageType.EVENT:
|
||||||
|
this.debug(`Recieved event ${data.event} with args`, data.args);
|
||||||
if (this.eventListeners.has(data.event)) {
|
if (this.eventListeners.has(data.event)) {
|
||||||
for (const listener of this.eventListeners.get(data.event)!) {
|
for (const listener of this.eventListeners.get(data.event)!) {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user