Compare commits

..

1 Commits

Author SHA1 Message Date
TrainDoctor bfd09520f9 Update aiohttp and others 2025-07-01 12:53:26 -07:00
45 changed files with 1062 additions and 1346 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ jobs:
working-directory: ./frontend
run: |
npm i -g pnpm
pnpm i --frozen-lockfile --dangerously-allow-all-builds
pnpm i --frozen-lockfile
- name: Build JS Frontend 🛠️
working-directory: ./frontend
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
working-directory: ./frontend
run: |
npm i -g pnpm
pnpm i --frozen-lockfile --dangerously-allow-all-builds
pnpm i --frozen-lockfile
- name: Build JS Frontend 🛠️
working-directory: ./frontend
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
working-directory: frontend
run: |
npm i -g pnpm
pnpm i --frozen-lockfile --dangerously-allow-all-builds
pnpm i --frozen-lockfile
- name: Run prettier (TypeScript)
working-directory: frontend
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
working-directory: frontend
run: |
npm i -g pnpm
pnpm i --frozen-lockfile --dangerously-allow-all-builds
pnpm i --frozen-lockfile
- name: Run pyright (Python)
uses: jakebailey/pyright-action@v1
+1 -1
View File
@@ -95,7 +95,7 @@ Please consult [the wiki page regarding development](https://wiki.deckbrew.xyz/e
1. In your clone of the repository, run these commands.
```bash
cd frontend
pnpm i # NOTE: you may need to approve esbuild's build script with pnpm approve-builds
pnpm i
pnpm run build
```
1. If you are modifying the UI, these commands will need to be run before deploying the changes to your Steam Deck.
+10 -31
View File
@@ -18,10 +18,9 @@ from enum import IntEnum
from typing import Dict, List, TypedDict
# Local modules
from .localplatform.localplatform import chown, chmod, get_chown_plugin_path
from .localplatform.localplatform import chown, chmod
from .loader import Loader, Plugins
from .helpers import get_ssl_context, download_remote_binary_to_path
from .enums import UserType
from .settings import SettingsManager
logger = getLogger("Browser")
@@ -61,6 +60,13 @@ class PluginBrowser:
return False
zip_file = ZipFile(zip)
zip_file.extractall(self.plugin_path)
plugin_folder = self.find_plugin_folder(name)
assert plugin_folder is not None
plugin_dir = path.join(self.plugin_path, plugin_folder)
if not chown(plugin_dir) or not chmod(plugin_dir, 555):
logger.error(f"chown/chmod exited with a non-zero exit code")
return False
return True
async def _download_remote_binaries_for_plugin_with_name(self, pluginBasePath: str):
@@ -95,6 +101,8 @@ class PluginBrowser:
rv = False
raise Exception(f"Error Downloading Remote Binary {binName}@{binURL} with hash {binHash} to {path.join(pluginBinPath, binName)}")
chown(self.plugin_path)
chmod(pluginBasePath, 555)
else:
rv = True
logger.info(f"No Remote Binaries to Download")
@@ -116,25 +124,6 @@ class PluginBrowser:
return folder
except:
logger.debug(f"skipping {folder}")
def set_plugin_dir_permissions(self, plugin_dir: str) -> bool:
plugin_json_path = path.join(plugin_dir, 'plugin.json')
logger.debug(f"Checking plugin.json at {plugin_json_path}")
root_plugin = False
if access(plugin_json_path, R_OK):
with open(plugin_json_path, "r", encoding="utf-8") as f:
plugin_json = json.load(f)
if "flags" in plugin_json and "root" in plugin_json["flags"]:
root_plugin = True
logger.debug("root_plugin %d, dir %s", root_plugin, plugin_dir)
if get_chown_plugin_path():
return chown(plugin_dir, UserType.EFFECTIVE_USER if root_plugin else UserType.HOST_USER, True) and chown(plugin_dir, UserType.EFFECTIVE_USER, False) and chmod(plugin_dir, 755) and chown(plugin_json_path, UserType.EFFECTIVE_USER, False) and chmod(plugin_json_path, 755)
else:
logger.debug("chown disabled by environment")
return True
async def uninstall_plugin(self, name: str):
if self.loader.watcher:
@@ -150,7 +139,6 @@ class PluginBrowser:
# plugins_snapshot = self.plugins.copy()
# snapshot_string = pformat(plugins_snapshot)
# logger.debug("current plugins: %s", snapshot_string)
if name in self.plugins:
logger.debug("Plugin %s was found", name)
await self.plugins[name].stop(uninstall=True)
@@ -278,7 +266,6 @@ class PluginBrowser:
plugin_dir = path.join(self.plugin_path, plugin_folder)
await self.loader.ws.emit("loader/plugin_download_info", 95, "Store.download_progress_info.download_remote")
ret = await self._download_remote_binaries_for_plugin_with_name(plugin_dir)
chown_ret = self.set_plugin_dir_permissions(plugin_dir)
if ret:
logger.info(f"Installed {name} (Version: {version})")
if name in self.loader.plugins:
@@ -291,9 +278,6 @@ class PluginBrowser:
self.settings.setSetting("pluginOrder", current_plugin_order)
logger.debug("Plugin %s was added to the pluginOrder setting", name)
await self.loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
elif not chown_ret:
logger.error("Could not chown plugin")
return
else:
logger.error("Could not download remote binaries")
return
@@ -346,10 +330,5 @@ class PluginBrowser:
if name in plugin_order:
plugin_order.remove(name)
self.settings.setSetting("pluginOrder", plugin_order)
disabled_plugins: List[str] = self.settings.getSetting("disabled_plugins", [])
if name in disabled_plugins:
disabled_plugins.remove(name)
self.settings.setSetting("disabled_plugins", disabled_plugins)
logger.debug("Removed any settings for plugin %s", name)
+3 -2
View File
@@ -1,8 +1,9 @@
from enum import IntEnum
class UserType(IntEnum):
HOST_USER = 1 # usually deck
EFFECTIVE_USER = 2 # usually root
HOST_USER = 1
EFFECTIVE_USER = 2
ROOT = 3
class PluginLoadType(IntEnum):
LEGACY_EVAL_IIFE = 0 # legacy, uses legacy serverAPI
+1 -2
View File
@@ -181,8 +181,7 @@ def get_user_group_id() -> int:
# Get the default home path unless a user is specified
def get_home_path(username: str | None = None) -> str:
# TODO hardcoded root is kinda a hack
return localplatform.get_home_path(UserType.EFFECTIVE_USER if username == "root" else UserType.HOST_USER)
return localplatform.get_home_path(UserType.ROOT if username == "root" else UserType.HOST_USER)
async def is_systemd_unit_active(unit_name: str) -> bool:
return await localplatform.service_active(unit_name)
+2 -7
View File
@@ -78,7 +78,6 @@ class Loader:
self.live_reload = live_reload
self.reload_queue: ReloadQueue = Queue()
self.loop.create_task(self.handle_reloads())
self.context: PluginManager = server_instance
if live_reload:
self.observer = Observer()
@@ -131,7 +130,7 @@ class Loader:
async def get_plugins(self):
plugins = list(self.plugins.values())
return [{"name": str(i), "version": i.version, "load_type": i.load_type, "disabled": i.disabled} 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"]]
@@ -165,10 +164,6 @@ class Loader:
await self.ws.emit(f"loader/plugin_event", {"plugin": plugin.name, "event": event, "args": args})
plugin = PluginWrapper(file, plugin_directory, self.plugin_path, plugin_emitted_event)
if hasattr(self.context, "utilities") and plugin.name in await self.context.utilities.get_setting("disabled_plugins",[]):
plugin.disabled = True
self.plugins[plugin.name] = plugin
return
if plugin.name in self.plugins:
if not "debug" in plugin.flags and refresh:
self.logger.info(f"Plugin {plugin.name} is already loaded and has requested to not be re-loaded")
@@ -188,7 +183,7 @@ class Loader:
print_exc()
async def dispatch_plugin(self, name: str, version: str | None, load_type: int = PluginLoadType.ESMODULE_V1.value):
await self.ws.emit("loader/import_plugin", name, version, load_type, True, 15000)
await self.ws.emit("loader/import_plugin", name, version, load_type)
async def import_plugins(self):
self.logger.info(f"import plugins from {self.plugin_path}")
+3 -18
View File
@@ -102,7 +102,6 @@
},
"no_hash": "This plugin does not have a hash, you are installing it at your own risk.",
"not_installed": "(not installed)",
"disabled": "The plugin will be re-enabled after installation",
"overwrite": {
"button_idle": "Overwrite",
"button_processing": "Overwriting",
@@ -134,13 +133,10 @@
"uninstall": "Uninstall",
"update_all_one": "Update 1 plugin",
"update_all_other": "Update {{count}} plugins",
"update_to": "Update to {{name}}",
"disable": "Disable",
"enable": "Enable"
"update_to": "Update to {{name}}"
},
"PluginListLabel": {
"hidden": "Hidden from the quick access menu",
"disabled": "Plugin disabled"
"hidden": "Hidden from the quick access menu"
},
"PluginLoader": {
"decky_title": "Decky",
@@ -156,23 +152,12 @@
"desc": "Are you sure you want to uninstall {{name}}?",
"title": "Uninstall {{name}}"
},
"plugin_disable": {
"button": "Disable",
"desc": "Are you sure you want to disable {{name}}?",
"title": "Disable {{name}}",
"error": "Error disabling {{name}}"
},
"plugin_enable": {
"error": "Error enabling {{name}}"
},
"plugin_update_one": "Updates available for 1 plugin!",
"plugin_update_other": "Updates available for {{count}} plugins!"
},
"PluginView": {
"hidden_one": "1 plugin is hidden from this list",
"hidden_other": "{{count}} plugins are hidden from this list",
"disabled_one": "1 plugin is disabled",
"disabled_other": "{{count}} plugins are disabled"
"hidden_other": "{{count}} plugins are hidden from this list"
},
"RemoteDebugging": {
"remote_cef": {
@@ -59,6 +59,8 @@ def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool =
user_str = _get_user()+":"+_get_user_group()
elif user == UserType.EFFECTIVE_USER:
user_str = _get_effective_user()+":"+_get_effective_user_group()
elif user == UserType.ROOT:
user_str = "root:root"
else:
raise Exception("Unknown User Type")
@@ -85,7 +87,7 @@ def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
return True
def file_owner(path : str) -> UserType|None:
def folder_owner(path : str) -> UserType|None:
user_owner = _get_user_owner(path)
if (user_owner == _get_user()):
@@ -104,14 +106,13 @@ def get_home_path(user : UserType = UserType.HOST_USER) -> str:
user_name = _get_user()
elif user == UserType.EFFECTIVE_USER:
user_name = _get_effective_user()
elif user == UserType.ROOT:
pass
else:
raise Exception("Unknown User Type")
return pwd.getpwnam(user_name).pw_dir
def get_effective_username() -> str:
return _get_effective_user()
def get_username() -> str:
return _get_user()
@@ -120,8 +121,8 @@ def setgid(user : UserType = UserType.HOST_USER):
if user == UserType.HOST_USER:
user_id = _get_user_group_id()
elif user == UserType.EFFECTIVE_USER:
pass # we already are
elif user == UserType.ROOT:
pass
else:
raise Exception("Unknown user type")
@@ -132,8 +133,8 @@ def setuid(user : UserType = UserType.HOST_USER):
if user == UserType.HOST_USER:
user_id = _get_user_id()
elif user == UserType.EFFECTIVE_USER:
pass # we already are
elif user == UserType.ROOT:
pass
else:
raise Exception("Unknown user type")
@@ -7,7 +7,7 @@ def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool =
def chmod(path : str, permissions : int, recursive : bool = True) -> bool:
return True # Stubbed
def file_owner(path : str) -> UserType|None:
def folder_owner(path : str) -> UserType|None:
return UserType.HOST_USER # Stubbed
def get_home_path(user : UserType = UserType.HOST_USER) -> str:
@@ -34,9 +34,6 @@ async def service_restart(service_name : str, block : bool = True) -> bool:
return True # Stubbed
def get_effective_username() -> str:
return os.getlogin()
def get_username() -> str:
return os.getlogin()
+1 -1
View File
@@ -50,7 +50,7 @@ def chown_plugin_dir():
if not path.exists(plugin_path): # For safety, create the folder before attempting to do anything with it
mkdir_as_user(plugin_path)
if not chown(plugin_path, UserType.EFFECTIVE_USER, False) or not chmod(plugin_path, 755, False):
if not chown(plugin_path, UserType.HOST_USER) or not chmod(plugin_path, 555):
logger.error(f"chown/chmod exited with a non-zero exit code")
if get_chown_plugin_path() == True:
+4 -20
View File
@@ -8,8 +8,7 @@ from traceback import format_exc
from .sandboxed_plugin import SandboxedPlugin
from .messages import MethodCallRequest, SocketMessageType
from ..enums import PluginLoadType, UserType
from ..localplatform.localplatform import file_owner, chown, chmod, get_chown_plugin_path
from ..enums import PluginLoadType
from ..localplatform.localsocket import LocalSocket
from ..helpers import get_homebrew_path, mkdir_as_user
@@ -27,12 +26,9 @@ class PluginWrapper:
self.load_type = PluginLoadType.LEGACY_EVAL_IIFE.value
plugin_dir_path = path.join(plugin_path, plugin_directory)
plugin_json_path = path.join(plugin_dir_path, "plugin.json")
json = load(open(plugin_json_path, "r", encoding="utf-8"))
if path.isfile(path.join(plugin_dir_path, "package.json")):
package_json = load(open(path.join(plugin_dir_path, "package.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")):
package_json = load(open(path.join(plugin_path, plugin_directory, "package.json"), "r", encoding="utf-8"))
self.version = package_json["version"]
if ("type" in package_json and package_json["type"] == "module"):
self.load_type = PluginLoadType.ESMODULE_V1.value
@@ -41,23 +37,11 @@ class PluginWrapper:
self.author = json["author"]
self.flags = json["flags"]
self.api_version = json["api_version"] if "api_version" in json else 0
self.disabled = False
self.passive = not path.isfile(self.file)
self.log = getLogger("plugin")
if get_chown_plugin_path():
# ensure plugin folder ownership
if file_owner(plugin_dir_path) != UserType.EFFECTIVE_USER:
chown(plugin_dir_path, UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER, True)
chown(plugin_dir_path, UserType.EFFECTIVE_USER, False)
chmod(plugin_dir_path, 755, True)
# fix plugin.json permissions
if file_owner(plugin_json_path) != UserType.EFFECTIVE_USER:
chown(plugin_json_path, UserType.EFFECTIVE_USER, False)
chmod(plugin_json_path, 755, False)
self.sandboxed_plugin = SandboxedPlugin(self.name, self.passive, self.flags, self.file, self.plugin_directory, self.plugin_path, self.version, self.author, self.api_version)
self.proc: Process | None = None
self._socket = LocalSocket()
@@ -13,8 +13,7 @@ from .messages import SocketResponseDict, SocketMessageType
from ..localplatform.localsocket import LocalSocket
from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path, ON_LINUX
from ..enums import UserType
from .. import helpers
from .. import settings # pyright: ignore [reportUnusedImport]
from .. import helpers, settings, injector # pyright: ignore [reportUnusedImport]
from typing import List, TypeVar, Any
@@ -62,10 +61,10 @@ class SandboxedPlugin:
if self.passive:
return
setgid(UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER)
setuid(UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER)
setgid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
setuid(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
# export a bunch of environment variables to help plugin developers
environ["HOME"] = get_home_path(UserType.EFFECTIVE_USER if "root" in self.flags else UserType.HOST_USER)
environ["HOME"] = get_home_path(UserType.ROOT if "root" in self.flags else UserType.HOST_USER)
environ["USER"] = "root" if "root" in self.flags else get_username()
environ["DECKY_VERSION"] = helpers.get_loader_version()
environ["DECKY_USER"] = get_username()
+3 -3
View File
@@ -1,7 +1,7 @@
from json import dump, load
from os import mkdir, path, listdir, rename
from typing import Any, Dict
from .localplatform.localplatform import chown, file_owner, get_chown_plugin_path
from .localplatform.localplatform import chown, folder_owner, get_chown_plugin_path
from .enums import UserType
from .helpers import get_homebrew_path
@@ -28,8 +28,8 @@ class SettingsManager:
#If the owner of the settings directory is not the user, then set it as the user:
expected_user = UserType.HOST_USER if get_chown_plugin_path() else UserType.EFFECTIVE_USER
if file_owner(settings_directory) != expected_user:
expected_user = UserType.HOST_USER if get_chown_plugin_path() else UserType.ROOT
if folder_owner(settings_directory) != expected_user:
chown(settings_directory, expected_user, False)
self.settings: Dict[str, Any] = {}
+4 -34
View File
@@ -1,5 +1,5 @@
from __future__ import annotations
from os import path, stat_result
from os import stat_result
import uuid
from urllib.parse import unquote
from json.decoder import JSONDecodeError
@@ -8,7 +8,7 @@ import re
from traceback import format_exc
from stat import FILE_ATTRIBUTE_HIDDEN # pyright: ignore [reportAttributeAccessIssue, reportUnknownVariableType]
from asyncio import StreamReader, StreamWriter, sleep, start_server, gather, open_connection
from asyncio import StreamReader, StreamWriter, start_server, gather, open_connection
from aiohttp import ClientSession, hdrs
from aiohttp.web import Request, StreamResponse, Response, json_response, post
from typing import TYPE_CHECKING, Callable, Coroutine, Dict, Any, List, TypedDict
@@ -80,8 +80,6 @@ class Utilities:
context.ws.add_route("utilities/restart_webhelper", self.restart_webhelper)
context.ws.add_route("utilities/close_cef_socket", self.close_cef_socket)
context.ws.add_route("utilities/_call_legacy_utility", self._call_legacy_utility)
context.ws.add_route("utilities/enable_plugin", self.enable_plugin)
context.ws.add_route("utilities/disable_plugin", self.disable_plugin)
context.web_app.add_routes([
post("/methods/{method_name}", self._handle_legacy_server_method_call)
@@ -216,7 +214,7 @@ class Utilities:
async def http_request_legacy(self, method: str, url: str, extra_opts: Any = {}, timeout: int | None = None):
async with ClientSession() as web:
res = await web.request(method, url, ssl=helpers.get_ssl_context(), timeout=timeout, **extra_opts) # type: ignore
res = await web.request(method, url, ssl=helpers.get_ssl_context(), timeout=timeout, **extra_opts)
text = await res.text()
return {
"status": res.status,
@@ -392,6 +390,7 @@ class Utilities:
"total": len(all),
}
# Based on https://stackoverflow.com/a/46422554/13174603
def start_rdt_proxy(self, ip: str, port: int):
async def pipe(reader: StreamReader, writer: StreamWriter):
@@ -475,32 +474,3 @@ class Utilities:
async def get_tab_id(self, name: str):
return (await get_tab(name)).id
async def disable_plugin(self, name: str):
disabled_plugins: List[str] = await self.get_setting("disabled_plugins", [])
if name not in disabled_plugins:
disabled_plugins.append(name)
await self.set_setting("disabled_plugins", disabled_plugins)
await self.context.plugin_loader.plugins[name].stop()
await self.context.ws.emit("loader/disable_plugin", name)
async def enable_plugin(self, name: str):
plugin_folder = self.context.plugin_browser.find_plugin_folder(name)
assert plugin_folder is not None
plugin_dir = path.join(self.context.plugin_browser.plugin_path, plugin_folder)
if name in self.context.plugin_loader.plugins:
plugin = self.context.plugin_loader.plugins[name]
if plugin.proc and plugin.proc.is_alive():
await plugin.stop()
self.context.plugin_loader.plugins.pop(name, None)
await sleep(1)
disabled_plugins: List[str] = await self.get_setting("disabled_plugins", [])
if name in disabled_plugins:
disabled_plugins.remove(name)
await self.set_setting("disabled_plugins", disabled_plugins)
await self.context.plugin_loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
+3 -1
View File
@@ -7,7 +7,7 @@ from aiohttp.web import Application, WebSocketResponse, Request, Response, get
from enum import IntEnum
from typing import Callable, Coroutine, Dict, Any, cast
from typing import Callable, Coroutine, Dict, Any, cast, TypeVar
from traceback import format_exc
@@ -29,6 +29,8 @@ class WSMessageExtra(WSMessage):
# see wsrouter.ts for typings
DataType = TypeVar("DataType")
Route = Callable[..., Coroutine[Any, Any, Any]]
class WSRouter:
+799 -744
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -14,9 +14,9 @@ include = [
]
[tool.poetry.dependencies]
python = ">=3.10,<3.14"
python = ">=3.10,<=3.13.5"
aiohttp = "^3.13.3"
aiohttp = "^3.10.11"
aiohttp-jinja2 = "^1.5.1"
aiohttp-cors = "^0.7.0"
watchdog = "^4"
+9 -9
View File
@@ -13,15 +13,15 @@
"localize": "i18next"
},
"devDependencies": {
"@decky/api": "^1.1.3",
"@decky/api": "^1.1.1",
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-image": "^3.0.3",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.7",
"@rollup/plugin-typescript": "^11.1.6",
"@types/react": "19.1.1",
"@types/react-dom": "19.1.1",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@types/react-file-icon": "^1.0.4",
"@types/react-router": "5.1.20",
"husky": "^9.0.11",
@@ -30,8 +30,8 @@
"inquirer": "^9.2.23",
"prettier": "^3.3.2",
"prettier-plugin-import-sort": "^0.0.7",
"react": "19.1.1",
"react-dom": "19.1.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"rollup": "^4.22.4",
"rollup-plugin-delete": "^2.0.0",
"rollup-plugin-external-globals": "^0.10.0",
@@ -47,13 +47,13 @@
}
},
"dependencies": {
"@decky/ui": "^4.11.1",
"@decky/ui": "^4.10.2",
"compare-versions": "^6.1.1",
"filesize": "^10.1.2",
"i18next": "^25.6.0",
"i18next": "^23.11.5",
"i18next-http-backend": "^2.5.2",
"react-file-icon": "^1.6.0",
"react-i18next": "^16.0.1",
"react-file-icon": "^1.5.0",
"react-i18next": "^14.1.2",
"react-icons": "^5.2.1",
"react-markdown": "^9.0.1",
"remark-gfm": "^4.0.0"
+82 -97
View File
@@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@decky/ui':
specifier: ^4.11.1
version: 4.11.1
specifier: ^4.10.2
version: 4.10.2
compare-versions:
specifier: ^6.1.1
version: 6.1.1
@@ -18,30 +18,30 @@ importers:
specifier: ^10.1.2
version: 10.1.2
i18next:
specifier: ^25.6.0
version: 25.6.0(typescript@5.4.5)
specifier: ^23.11.5
version: 23.11.5
i18next-http-backend:
specifier: ^2.5.2
version: 2.5.2
react-file-icon:
specifier: ^1.6.0
version: 1.6.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
specifier: ^1.5.0
version: 1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-i18next:
specifier: ^16.0.1
version: 16.0.1(i18next@25.6.0(typescript@5.4.5))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.4.5)
specifier: ^14.1.2
version: 14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-icons:
specifier: ^5.2.1
version: 5.2.1(react@19.1.1)
version: 5.2.1(react@18.3.1)
react-markdown:
specifier: ^9.0.1
version: 9.0.1(@types/react@19.1.1)(react@19.1.1)
version: 9.0.1(@types/react@18.3.3)(react@18.3.1)
remark-gfm:
specifier: ^4.0.0
version: 4.0.0
devDependencies:
'@decky/api':
specifier: ^1.1.3
version: 1.1.3
specifier: ^1.1.1
version: 1.1.1
'@rollup/plugin-commonjs':
specifier: ^26.0.1
version: 26.0.1(rollup@4.22.4)
@@ -61,11 +61,11 @@ importers:
specifier: ^11.1.6
version: 11.1.6(rollup@4.22.4)(tslib@2.6.3)(typescript@5.4.5)
'@types/react':
specifier: 19.1.1
version: 19.1.1
specifier: 18.3.3
version: 18.3.3
'@types/react-dom':
specifier: 19.1.1
version: 19.1.1(@types/react@19.1.1)
specifier: 18.3.0
version: 18.3.0
'@types/react-file-icon':
specifier: ^1.0.4
version: 1.0.4
@@ -91,11 +91,11 @@ importers:
specifier: ^0.0.7
version: 0.0.7(prettier@3.3.2)
react:
specifier: 19.1.1
version: 19.1.1
specifier: 18.3.1
version: 18.3.1
react-dom:
specifier: 19.1.1
version: 19.1.1(react@19.1.1)
specifier: 18.3.1
version: 18.3.1(react@18.3.1)
rollup:
specifier: ^4.22.4
version: 4.22.4
@@ -203,10 +203,6 @@ packages:
resolution: {integrity: sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==}
engines: {node: '>=6.9.0'}
'@babel/runtime@7.28.4':
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
engines: {node: '>=6.9.0'}
'@babel/template@7.24.7':
resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==}
engines: {node: '>=6.9.0'}
@@ -219,11 +215,11 @@ packages:
resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==}
engines: {node: '>=6.9.0'}
'@decky/api@1.1.3':
resolution: {integrity: sha512-XsPCZxfxk5I1UtylIUN3qaWQI31siQbKfbLIskkI5innEatY1m4NQqBv/6hwPaO9mKMbdqYpnh5PSJDeMEOOBA==}
'@decky/api@1.1.1':
resolution: {integrity: sha512-R5fkBRHBt5QIQY7Q0AlbVIhlIZ/nTzwBOoi8Rt4Go2fjFnoMKPInCJl6cPjXzimGwl2pyqKJgY6VnH6ar0XrHQ==}
'@decky/ui@4.11.1':
resolution: {integrity: sha512-+x+rJB0MBQSQGp0UpsRJ4BOv7zlHzcBPT76enopjGf56H41beR8VZua9F4upLtDgPku4Zh8z3uB53nFlvTilXQ==}
'@decky/ui@4.10.2':
resolution: {integrity: sha512-dfY/OEI/rhG4d3Tvx4Y3TLmBrJ+nAm2RTbkoOJ3VAglql3Lu7RY2ixeDFbs21ZWWzWh/C+9dAQKjQ4lx4d1f2g==}
'@esbuild/aix-ppc64@0.20.2':
resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==}
@@ -602,10 +598,11 @@ packages:
'@types/node@20.14.2':
resolution: {integrity: sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==}
'@types/react-dom@19.1.1':
resolution: {integrity: sha512-jFf/woGTVTjUJsl2O7hcopJ1r0upqoq/vIOoCj0yLh3RIXxWcljlpuZ+vEBRXsymD1jhfeJrlyTy/S1UW+4y1w==}
peerDependencies:
'@types/react': ^19.0.0
'@types/prop-types@15.7.12':
resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==}
'@types/react-dom@18.3.0':
resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==}
'@types/react-file-icon@1.0.4':
resolution: {integrity: sha512-c1mIklUDaxm9odxf8RTiy/EAxsblZliJ86EKIOAyuafP9eK3iudyn4ATv53DX6ZvgGymc7IttVNm97LTGnTiYA==}
@@ -613,8 +610,8 @@ packages:
'@types/react-router@5.1.20':
resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==}
'@types/react@19.1.1':
resolution: {integrity: sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==}
'@types/react@18.3.3':
resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==}
'@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
@@ -1190,16 +1187,8 @@ packages:
engines: {node: '>=18.0.0 || >=20.0.0 || >=22.0.0', npm: '>=6', yarn: '>=1'}
hasBin: true
i18next@23.16.8:
resolution: {integrity: sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==}
i18next@25.6.0:
resolution: {integrity: sha512-tTn8fLrwBYtnclpL5aPXK/tAYBLWVvoHM1zdfXoRNLcI+RvtMsoZRV98ePlaW3khHYKuNh/Q65W/+NVFUeIwVw==}
peerDependencies:
typescript: ^5
peerDependenciesMeta:
typescript:
optional: true
i18next@23.11.5:
resolution: {integrity: sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==}
iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
@@ -1720,32 +1709,29 @@ packages:
quick-temp@0.1.8:
resolution: {integrity: sha512-YsmIFfD9j2zaFwJkzI6eMG7y0lQP7YeWzgtFgNl38pGWZBSXJooZbOWwkcRot7Vt0Fg9L23pX0tqWU3VvLDsiA==}
react-dom@19.1.1:
resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==}
react-dom@18.3.1:
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
peerDependencies:
react: ^19.1.1
react: ^18.3.1
react-file-icon@1.6.0:
resolution: {integrity: sha512-Ba4Qa2ya/kvhcCd4LJja77sV7JD7u1ZXcI1DUz+TII3nGmglG6QY+NZeHizThokgct3qI0glwb9eV8NqRGs5lw==}
react-file-icon@1.5.0:
resolution: {integrity: sha512-6K2/nAI69CS838HOS+4S95MLXwf1neWywek1FgqcTFPTYjnM8XT7aBLz4gkjoqQKY9qPhu3A2tu+lvxhmZYY9w==}
peerDependencies:
react: ^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.2.0
react-dom: ^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.2.0
react: ^18.0.0 || ^17.0.0 || ^16.2.0
react-dom: ^18.0.0 || ^17.0.0 || ^16.2.0
react-i18next@16.0.1:
resolution: {integrity: sha512-0S//bpYEkCPjzuVmxDf9Z6+Y+ArNvpAUk7eDL4qNCZXjDh6Z9j6MZ+NThU7kMCOsmYmDCun3GYEwkiOjjZo9Ug==}
react-i18next@14.1.2:
resolution: {integrity: sha512-FSIcJy6oauJbGEXfhUgVeLzvWBhIBIS+/9c6Lj4niwKZyGaGb4V4vUbATXSlsHJDXXB+ociNxqFNiFuV1gmoqg==}
peerDependencies:
i18next: '>= 25.5.2'
i18next: '>= 23.2.3'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
typescript: ^5
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
typescript:
optional: true
react-icons@5.2.1:
resolution: {integrity: sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==}
@@ -1761,8 +1747,8 @@ packages:
'@types/react': '>=18'
react: '>=18'
react@19.1.1:
resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==}
react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
readable-stream@2.3.8:
@@ -1887,8 +1873,8 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
scheduler@0.26.0:
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
@@ -2280,8 +2266,6 @@ snapshots:
dependencies:
regenerator-runtime: 0.14.1
'@babel/runtime@7.28.4': {}
'@babel/template@7.24.7':
dependencies:
'@babel/code-frame': 7.24.7
@@ -2309,9 +2293,9 @@ snapshots:
'@babel/helper-validator-identifier': 7.24.7
to-fast-properties: 2.0.0
'@decky/api@1.1.3': {}
'@decky/api@1.1.1': {}
'@decky/ui@4.11.1': {}
'@decky/ui@4.10.2': {}
'@esbuild/aix-ppc64@0.20.2':
optional: true
@@ -2583,21 +2567,24 @@ snapshots:
dependencies:
undici-types: 5.26.5
'@types/react-dom@19.1.1(@types/react@19.1.1)':
'@types/prop-types@15.7.12': {}
'@types/react-dom@18.3.0':
dependencies:
'@types/react': 19.1.1
'@types/react': 18.3.3
'@types/react-file-icon@1.0.4':
dependencies:
'@types/react': 19.1.1
'@types/react': 18.3.3
'@types/react-router@5.1.20':
dependencies:
'@types/history': 4.7.11
'@types/react': 19.1.1
'@types/react': 18.3.3
'@types/react@19.1.1':
'@types/react@18.3.3':
dependencies:
'@types/prop-types': 15.7.12
csstype: 3.1.3
'@types/resolve@1.20.2': {}
@@ -3242,7 +3229,7 @@ snapshots:
esbuild: 0.20.2
fs-extra: 11.2.0
gulp-sort: 2.0.0
i18next: 23.16.8
i18next: 23.11.5
js-yaml: 4.1.0
lilconfig: 3.1.2
rsvp: 4.8.5
@@ -3253,16 +3240,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
i18next@23.16.8:
i18next@23.11.5:
dependencies:
'@babel/runtime': 7.24.7
i18next@25.6.0(typescript@5.4.5):
dependencies:
'@babel/runtime': 7.28.4
optionalDependencies:
typescript: 5.4.5
iconv-lite@0.4.24:
dependencies:
safer-buffer: 2.1.2
@@ -3979,43 +3960,43 @@ snapshots:
rimraf: 2.7.1
underscore.string: 3.3.6
react-dom@19.1.1(react@19.1.1):
react-dom@18.3.1(react@18.3.1):
dependencies:
react: 19.1.1
scheduler: 0.26.0
loose-envify: 1.4.0
react: 18.3.1
scheduler: 0.23.2
react-file-icon@1.6.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
react-file-icon@1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
colord: 2.9.3
prop-types: 15.8.1
react: 19.1.1
react-dom: 19.1.1(react@19.1.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-i18next@16.0.1(i18next@25.6.0(typescript@5.4.5))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.4.5):
react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@babel/runtime': 7.28.4
'@babel/runtime': 7.24.7
html-parse-stringify: 3.0.1
i18next: 25.6.0(typescript@5.4.5)
react: 19.1.1
i18next: 23.11.5
react: 18.3.1
optionalDependencies:
react-dom: 19.1.1(react@19.1.1)
typescript: 5.4.5
react-dom: 18.3.1(react@18.3.1)
react-icons@5.2.1(react@19.1.1):
react-icons@5.2.1(react@18.3.1):
dependencies:
react: 19.1.1
react: 18.3.1
react-is@16.13.1: {}
react-markdown@9.0.1(@types/react@19.1.1)(react@19.1.1):
react-markdown@9.0.1(@types/react@18.3.3)(react@18.3.1):
dependencies:
'@types/hast': 3.0.4
'@types/react': 19.1.1
'@types/react': 18.3.3
devlop: 1.1.0
hast-util-to-jsx-runtime: 2.3.0
html-url-attributes: 3.0.0
mdast-util-to-hast: 13.2.0
react: 19.1.1
react: 18.3.1
remark-parse: 11.0.0
remark-rehype: 11.1.0
unified: 11.0.4
@@ -4024,7 +4005,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
react@19.1.1: {}
react@18.3.1:
dependencies:
loose-envify: 1.4.0
readable-stream@2.3.8:
dependencies:
@@ -4181,7 +4164,9 @@ snapshots:
safer-buffer@2.1.2: {}
scheduler@0.26.0: {}
scheduler@0.23.2:
dependencies:
loose-envify: 1.4.0
semver@6.3.1: {}
-1
View File
@@ -23,7 +23,6 @@ export default defineConfig([
}),
externalGlobals({
react: 'SP_REACT',
'react/jsx-runtime': 'SP_JSX',
'react-dom': 'SP_REACTDOM',
// hack to shut up react-markdown
process: '{cwd: () => {}}',
+1 -17
View File
@@ -1,14 +1,12 @@
import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
import { DEFAULT_NOTIFICATION_SETTINGS, NotificationSettings } from '../notification-service';
import { DisabledPlugin, Plugin } from '../plugin';
import { Plugin } from '../plugin';
import { PluginUpdateMapping } from '../store';
import { VerInfo } from '../updater';
interface PublicDeckyState {
plugins: Plugin[];
disabledPlugins: DisabledPlugin[];
installedPlugins: (Plugin | DisabledPlugin)[];
pluginOrder: string[];
frozenPlugins: string[];
hiddenPlugins: string[];
@@ -28,8 +26,6 @@ export interface UserInfo {
export class DeckyState {
private _plugins: Plugin[] = [];
private _disabledPlugins: DisabledPlugin[] = [];
private _installedPlugins: (Plugin | DisabledPlugin)[] = [];
private _pluginOrder: string[] = [];
private _frozenPlugins: string[] = [];
private _hiddenPlugins: string[] = [];
@@ -46,8 +42,6 @@ export class DeckyState {
publicState(): PublicDeckyState {
return {
plugins: this._plugins,
disabledPlugins: this._disabledPlugins,
installedPlugins: this._installedPlugins,
pluginOrder: this._pluginOrder,
frozenPlugins: this._frozenPlugins,
hiddenPlugins: this._hiddenPlugins,
@@ -68,13 +62,6 @@ export class DeckyState {
setPlugins(plugins: Plugin[]) {
this._plugins = plugins;
this._installedPlugins = [...plugins, ...this._disabledPlugins];
this.notifyUpdate();
}
setDisabledPlugins(disabledPlugins: DisabledPlugin[]) {
this._disabledPlugins = disabledPlugins;
this._installedPlugins = [...this._plugins, ...disabledPlugins];
this.notifyUpdate();
}
@@ -138,7 +125,6 @@ interface DeckyStateContext extends PublicDeckyState {
setIsLoaderUpdating(hasUpdate: boolean): void;
setActivePlugin(name: string): void;
setPluginOrder(pluginOrder: string[]): void;
setDisabledPlugins(disabled: DisabledPlugin[]): void;
closeActivePlugin(): void;
}
@@ -177,7 +163,6 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
const setActivePlugin = deckyState.setActivePlugin.bind(deckyState);
const closeActivePlugin = deckyState.closeActivePlugin.bind(deckyState);
const setPluginOrder = deckyState.setPluginOrder.bind(deckyState);
const setDisabledPlugins = deckyState.setDisabledPlugins.bind(deckyState);
return (
<DeckyStateContext.Provider
@@ -188,7 +173,6 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
setActivePlugin,
closeActivePlugin,
setPluginOrder,
setDisabledPlugins,
}}
>
{children}
+10 -36
View File
@@ -1,7 +1,7 @@
import { ButtonItem, ErrorBoundary, Focusable, PanelSection, PanelSectionRow } from '@decky/ui';
import { FC, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { FaBan, FaEyeSlash } from 'react-icons/fa';
import { FaEyeSlash } from 'react-icons/fa';
import { useDeckyState } from './DeckyState';
import NotificationBadge from './NotificationBadge';
@@ -9,16 +9,8 @@ import { useQuickAccessVisible } from './QuickAccessVisibleState';
import TitleView from './TitleView';
const PluginView: FC = () => {
const {
plugins,
disabledPlugins,
hiddenPlugins,
updates,
activePlugin,
pluginOrder,
setActivePlugin,
closeActivePlugin,
} = useDeckyState();
const { plugins, hiddenPlugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } =
useDeckyState();
const visible = useQuickAccessVisible();
const { t } = useTranslation();
@@ -29,9 +21,7 @@ const PluginView: FC = () => {
.sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name))
.filter((p) => p.content)
.filter(({ name }) => !hiddenPlugins.includes(name));
}, [plugins, pluginOrder, hiddenPlugins]);
const numberOfHidden = hiddenPlugins.filter((name) => !!plugins.find((p) => p.name === name)).length;
}, [plugins, pluginOrder]);
if (activePlugin) {
return (
@@ -63,28 +53,12 @@ const PluginView: FC = () => {
</ButtonItem>
</PanelSectionRow>
))}
<div
style={{
display: 'flex',
flexDirection: 'column',
position: 'absolute',
justifyContent: 'center',
padding: '5px 0px',
}}
>
{numberOfHidden > 0 && (
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem' }}>
<FaEyeSlash />
<div>{t('PluginView.hidden', { count: numberOfHidden })}</div>
</div>
)}
{disabledPlugins.length > 0 && (
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem' }}>
<FaBan />
<div>{t('PluginView.disabled', { count: disabledPlugins.length })}</div>
</div>
)}
</div>
{hiddenPlugins.length > 0 && (
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem', marginTop: '10px' }}>
<FaEyeSlash />
<div>{t('PluginView.hidden', { count: hiddenPlugins.length })}</div>
</div>
)}
</PanelSection>
</div>
</>
@@ -1,10 +1,10 @@
import { FC, PropsWithChildren, createContext, useContext, useState } from 'react';
import { FC, ReactNode, createContext, useContext, useState } from 'react';
const QuickAccessVisibleState = createContext<boolean>(false);
export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState);
export const QuickAccessVisibleStateProvider: FC<PropsWithChildren<{ tab: any }>> = ({ children, tab }) => {
export const QuickAccessVisibleStateProvider: FC<{ tab: any; children: ReactNode }> = ({ children, tab }) => {
const initial = tab.initialVisibility;
const [visible, setVisible] = useState<boolean>(initial);
// HACK but i can't think of a better way to do this
+1 -1
View File
@@ -10,7 +10,7 @@ interface WithSuspenseProps {
const WithSuspense: FunctionComponent<WithSuspenseProps> = (props) => {
const propsCopy = { ...props };
delete propsCopy.children;
(props.children as ReactElement<any>)?.props && Object.assign((props.children as ReactElement<any>).props, propsCopy); // There is probably a better way to do this but valve does it this way so ¯\_(ツ)_/¯
(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={
@@ -3,11 +3,10 @@ import { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaCheck, FaDownload } from 'react-icons/fa';
import { DisabledPlugin, InstallType, InstallTypeTranslationMapping } from '../../plugin';
import { InstallType, InstallTypeTranslationMapping } from '../../plugin';
interface MultiplePluginsInstallModalProps {
requests: { name: string; version: string; hash: string; install_type: InstallType }[];
disabledPlugins: DisabledPlugin[];
onOK(): void | Promise<void>;
onCancel(): void | Promise<void>;
closeModal?(): void;
@@ -18,7 +17,6 @@ type TitleTranslationMapping = 'mixed' | (typeof InstallTypeTranslationMapping)[
const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
requests,
disabledPlugins,
onOK,
onCancel,
closeModal,
@@ -118,11 +116,10 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
version,
});
const disabled = disabledPlugins.some((p) => p.name === name);
return (
<li key={i} style={{ display: 'flex', flexDirection: 'column' }}>
<span>
{disabled ? `${description} - ${t('PluginInstallModal.disabled')}` : description}{' '}
{description}{' '}
{(pluginsCompleted.includes(name) && <FaCheck />) || (name === pluginInProgress && <FaDownload />)}
</span>
{hash === 'False' && (
@@ -1,39 +0,0 @@
import { ConfirmModal, Spinner } from '@decky/ui';
import { FC, useState } from 'react';
import { disablePlugin } from '../../plugin';
interface PluginDisableModalProps {
name: string;
title: string;
buttonText: string;
description: string;
closeModal?(): void;
}
const PluginDisableModal: FC<PluginDisableModalProps> = ({ name, title, buttonText, description, closeModal }) => {
const [disabling, setDisabling] = useState<boolean>(false);
return (
<ConfirmModal
closeModal={closeModal}
onOK={async () => {
setDisabling(true);
await disablePlugin(name);
closeModal?.();
}}
bOKDisabled={disabling}
bCancelDisabled={disabling}
strTitle={
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', width: '100%' }}>
{title}
{disabling && <Spinner width="24px" height="24px" style={{ marginLeft: 'auto' }} />}
</div>
}
strOKButtonText={buttonText}
>
{description}
</ConfirmModal>
);
};
export default PluginDisableModal;
@@ -9,7 +9,6 @@ interface PluginInstallModalProps {
version: string;
hash: string;
installType: InstallType;
disabled?: boolean;
onOK(): void;
onCancel(): void;
closeModal?(): void;
@@ -20,7 +19,6 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
version,
hash,
installType,
disabled,
onOK,
onCancel,
closeModal,
@@ -47,10 +45,6 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
}, []);
const installTypeTranslationKey = InstallTypeTranslationMapping[installType];
const description = t(`PluginInstallModal.${installTypeTranslationKey}.desc`, {
artifact: artifact,
version: version,
});
return (
<ConfirmModal
@@ -124,7 +118,10 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
// t('PluginInstallModal.update.desc')
// t('PluginInstallModal.downgrade.desc')
// t('PluginInstallModal.overwrite.desc')
disabled ? `${description} ${t('PluginInstallModal.disabled')}` : description
t(`PluginInstallModal.${installTypeTranslationKey}.desc`, {
artifact: artifact,
version: version,
})
}
</div>
{hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>}
@@ -2,10 +2,8 @@ import { ConfirmModal, Spinner } from '@decky/ui';
import { FC, useState } from 'react';
import { uninstallPlugin } from '../../plugin';
import { DeckyState } from '../DeckyState';
interface PluginUninstallModalProps {
deckyState: DeckyState;
name: string;
title: string;
buttonText: string;
@@ -13,14 +11,7 @@ interface PluginUninstallModalProps {
closeModal?(): void;
}
const PluginUninstallModal: FC<PluginUninstallModalProps> = ({
name,
title,
buttonText,
description,
deckyState,
closeModal,
}) => {
const PluginUninstallModal: FC<PluginUninstallModalProps> = ({ name, title, buttonText, description, closeModal }) => {
const [uninstalling, setUninstalling] = useState<boolean>(false);
return (
<ConfirmModal
@@ -28,7 +19,6 @@ const PluginUninstallModal: FC<PluginUninstallModalProps> = ({
onOK={async () => {
setUninstalling(true);
await uninstallPlugin(name);
deckyState.setDisabledPlugins(deckyState.publicState().disabledPlugins.filter((d) => d.name !== name));
// uninstalling a plugin resets the hidden setting for it server-side
// we invalidate here so if you re-install it, you won't have an out-of-date hidden filter
await DeckyPluginLoader.frozenPluginsService.invalidate();
@@ -1,4 +1,4 @@
import { FC, JSX, useEffect, useState } from 'react';
import { FC, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IconContext } from 'react-icons';
import { FaExclamationTriangle, FaQuestionCircle, FaUserSlash } from 'react-icons/fa';
@@ -1,16 +1,15 @@
import { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { FaBan, FaEyeSlash, FaLock } from 'react-icons/fa';
import { FaEyeSlash, FaLock } from 'react-icons/fa';
interface PluginListLabelProps {
frozen: boolean;
hidden: boolean;
disabled: boolean;
name: string;
version?: string;
}
const PluginListLabel: FC<PluginListLabelProps> = ({ name, frozen, hidden, version, disabled }) => {
const PluginListLabel: FC<PluginListLabelProps> = ({ name, frozen, hidden, version }) => {
const { t } = useTranslation();
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
@@ -44,20 +43,6 @@ const PluginListLabel: FC<PluginListLabelProps> = ({ name, frozen, hidden, versi
{t('PluginListLabel.hidden')}
</div>
)}
{disabled && (
<div
style={{
fontSize: '0.8rem',
color: '#dcdedf',
display: 'flex',
alignItems: 'center',
gap: '10px',
}}
>
<FaBan />
{t('PluginListLabel.disabled')}
</div>
)}
</div>
);
};
@@ -2,11 +2,9 @@ import {
DialogBody,
DialogButton,
DialogControlsSection,
Focusable,
GamepadEvent,
Menu,
MenuItem,
NavEntryPositionPreferences,
ReorderableEntry,
ReorderableList,
showContextMenu,
@@ -15,7 +13,7 @@ import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaDownload, FaEllipsisH, FaRecycle } from 'react-icons/fa';
import { InstallType, enablePlugin } from '../../../../plugin';
import { InstallType } from '../../../../plugin';
import {
StorePluginVersion,
getPluginList,
@@ -37,7 +35,6 @@ async function reinstallPlugin(pluginName: string, currentVersion?: string) {
type PluginTableData = PluginData & {
name: string;
disabled: boolean;
frozen: boolean;
onFreeze(): void;
onUnfreeze(): void;
@@ -57,25 +54,22 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
return null;
}
const { name, update, version, onHide, onShow, hidden, onFreeze, onUnfreeze, frozen, isDeveloper, disabled } =
props.entry.data;
const { name, update, version, onHide, onShow, hidden, onFreeze, onUnfreeze, frozen, isDeveloper } = props.entry.data;
const showCtxMenu = (e: MouseEvent | GamepadEvent) => {
showContextMenu(
<Menu label={t('PluginListIndex.plugin_actions')}>
{!disabled && (
<MenuItem
onSelected={async () => {
try {
await reloadPluginBackend(name);
} catch (err) {
console.error('Error Reloading Plugin Backend', err);
}
}}
>
{t('PluginListIndex.reload')}
</MenuItem>
)}
<MenuItem
onSelected={async () => {
try {
await reloadPluginBackend(name);
} catch (err) {
console.error('Error Reloading Plugin Backend', err);
}
}}
>
{t('PluginListIndex.reload')}
</MenuItem>
<MenuItem
onSelected={() =>
DeckyPluginLoader.uninstallPlugin(
@@ -88,28 +82,11 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
>
{t('PluginListIndex.uninstall')}
</MenuItem>
{disabled ? (
<MenuItem onSelected={() => enablePlugin(name)}>{t('PluginListIndex.enable')}</MenuItem>
{hidden ? (
<MenuItem onSelected={onShow}>{t('PluginListIndex.show')}</MenuItem>
) : (
<MenuItem
onSelected={() =>
DeckyPluginLoader.disablePlugin(
name,
t('PluginLoader.plugin_disable.title', { name }),
t('PluginLoader.plugin_disable.button'),
t('PluginLoader.plugin_disable.desc', { name }),
)
}
>
{t('PluginListIndex.disable')}
</MenuItem>
<MenuItem onSelected={onHide}>{t('PluginListIndex.hide')}</MenuItem>
)}
{!disabled &&
(hidden ? (
<MenuItem onSelected={onShow}>{t('PluginListIndex.show')}</MenuItem>
) : (
<MenuItem onSelected={onHide}>{t('PluginListIndex.hide')}</MenuItem>
))}
{frozen ? (
<MenuItem onSelected={onUnfreeze}>{t('PluginListIndex.unfreeze')}</MenuItem>
) : (
@@ -121,7 +98,7 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
};
return (
<Focusable navEntryPreferPosition={NavEntryPositionPreferences.MAINTAIN_X} style={{ display: 'flex' }}>
<>
{update ? (
<DialogButton
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
@@ -160,7 +137,7 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
>
<FaEllipsisH />
</DialogButton>
</Focusable>
</>
);
}
@@ -170,18 +147,16 @@ type PluginData = {
};
export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) {
const { installedPlugins, disabledPlugins, updates, pluginOrder, setPluginOrder, frozenPlugins, hiddenPlugins } =
useDeckyState();
const { plugins, updates, pluginOrder, setPluginOrder, frozenPlugins, hiddenPlugins } = useDeckyState();
const [_, setPluginOrderSetting] = useSetting<string[]>(
'pluginOrder',
installedPlugins.map((plugin) => plugin.name),
plugins.map((plugin) => plugin.name),
);
const { t } = useTranslation();
useEffect(() => {
DeckyPluginLoader.checkPluginUpdates();
}, [installedPlugins, frozenPlugins]);
}, []);
const [pluginEntries, setPluginEntries] = useState<ReorderableEntry<PluginTableData>[]>([]);
const hiddenPluginsService = DeckyPluginLoader.hiddenPluginsService;
@@ -189,24 +164,15 @@ export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) {
useEffect(() => {
setPluginEntries(
installedPlugins.map(({ name, version }) => {
plugins.map(({ name, version }) => {
const frozen = frozenPlugins.includes(name);
const hidden = hiddenPlugins.includes(name);
return {
label: (
<PluginListLabel
name={name}
frozen={frozen}
hidden={hidden}
version={version}
disabled={disabledPlugins.find((p) => p.name == name) !== undefined}
/>
),
label: <PluginListLabel name={name} frozen={frozen} hidden={hidden} version={version} />,
position: pluginOrder.indexOf(name),
data: {
name,
disabled: disabledPlugins.some((disabledPlugin) => disabledPlugin.name === name),
frozen,
hidden,
isDeveloper,
@@ -220,9 +186,9 @@ export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) {
};
}),
);
}, [installedPlugins, updates, hiddenPlugins, disabledPlugins]);
}, [plugins, updates, hiddenPlugins]);
if (installedPlugins.length === 0) {
if (plugins.length === 0) {
return (
<div>
<p>{t('PluginListIndex.no_plugin')}</p>
@@ -4,7 +4,6 @@ import {
DialogControlsSection,
Field,
Focusable,
NavEntryPositionPreferences,
Navigation,
ProgressBar,
SteamSpinner,
@@ -88,10 +87,7 @@ export default function TestingVersionList() {
</>
}
>
<Focusable
style={{ height: '40px', marginLeft: 'auto', display: 'flex' }}
navEntryPreferPosition={NavEntryPositionPreferences.MAINTAIN_X}
>
<Focusable style={{ height: '40px', marginLeft: 'auto', display: 'flex' }}>
<DialogButton
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
onClick={async () => {
+4 -15
View File
@@ -1,23 +1,15 @@
import {
ButtonItem,
Dropdown,
Focusable,
NavEntryPositionPreferences,
PanelSectionRow,
SingleDropdownOption,
SuspensefulImage,
} from '@decky/ui';
import { ButtonItem, Dropdown, Focusable, PanelSectionRow, SingleDropdownOption, SuspensefulImage } from '@decky/ui';
import { CSSProperties, FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaArrowDown, FaArrowUp, FaCheck, FaDownload, FaRecycle } from 'react-icons/fa';
import { DisabledPlugin, InstallType, Plugin } from '../../plugin';
import { InstallType, Plugin } from '../../plugin';
import { StorePlugin, requestPluginInstall } from '../../store';
import ExternalLink from '../ExternalLink';
interface PluginCardProps {
storePlugin: StorePlugin;
installedPlugin: Plugin | DisabledPlugin | undefined;
installedPlugin: Plugin | undefined;
}
const PluginCard: FC<PluginCardProps> = ({ storePlugin, installedPlugin }) => {
@@ -147,10 +139,7 @@ const PluginCard: FC<PluginCardProps> = ({ storePlugin, installedPlugin }) => {
</div>
<div className="deckyStoreCardButtonRow">
<PanelSectionRow>
<Focusable
style={{ display: 'flex', gap: '5px', padding: 0 }}
navEntryPreferPosition={NavEntryPositionPreferences.MAINTAIN_X}
>
<Focusable style={{ display: 'flex', gap: '5px', padding: 0 }}>
<div
className="deckyStoreCardInstallContainer"
style={
+1 -1
View File
@@ -105,7 +105,7 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }>
})();
}, []);
const { installedPlugins } = useDeckyState();
const { plugins: installedPlugins } = useDeckyState();
return (
<>
+5 -13
View File
@@ -1,4 +1,8 @@
// Sets up DFL, then loads start.ts which starts up the loader
interface Window {
// Shut up TS
SP_REACTDOM: any;
}
(async () => {
console.debug('[Decky:Boot] Frontend init');
@@ -17,19 +21,7 @@
// deliberate partial import
const DFLWebpack = await import('@decky/ui/dist/webpack');
window.SP_REACT = DFLWebpack.findModule((m) => m.Component && m.PureComponent && m.useLayoutEffect);
window.SP_REACTDOM =
DFLWebpack.findModule((m) => m.createPortal && m.createRoot) ||
DFLWebpack.findModule((m) => m.createPortal && m.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE);
console.debug('[Decky:Boot] Setting up JSX internals...');
const jsx = DFLWebpack.findModule((m) => m.jsx && Object.keys(m).length == 1)?.jsx;
if (jsx) {
window.SP_JSX = {
jsx,
jsxs: jsx,
Fragment: window.SP_REACT.Fragment,
};
}
window.SP_REACTDOM = DFLWebpack.findModule((m) => m.createPortal && m.createRoot);
}
console.debug('[Decky:Boot] Setting up @decky/ui...');
window.DFL = await import('@decky/ui');
+37 -89
View File
@@ -19,7 +19,6 @@ import { DeckyState, DeckyStateContextProvider, UserInfo, useDeckyState } from '
import { File, FileSelectionType } from './components/modals/filepicker';
import { deinitFilepickerPatches, initFilepickerPatches } from './components/modals/filepicker/patches';
import MultiplePluginsInstallModal from './components/modals/MultiplePluginsInstallModal';
import PluginDisableModal from './components/modals/PluginDisableModal';
import PluginInstallModal from './components/modals/PluginInstallModal';
import PluginUninstallModal from './components/modals/PluginUninstallModal';
import NotificationBadge from './components/NotificationBadge';
@@ -31,7 +30,7 @@ import { FrozenPluginService } from './frozen-plugins-service';
import { HiddenPluginsService } from './hidden-plugins-service';
import Logger from './logger';
import { NotificationService } from './notification-service';
import { DisabledPlugin, InstallType, Plugin, PluginLoadType } from './plugin';
import { InstallType, Plugin, PluginLoadType } from './plugin';
import RouterHook from './router-hook';
import { deinitSteamFixes, initSteamFixes } from './steamfixes';
import { checkForPluginUpdates } from './store';
@@ -92,7 +91,6 @@ class PluginLoader extends Logger {
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/disable_plugin', this.doDisablePlugin.bind(this));
DeckyBackend.addEventListener('loader/add_plugin_install_prompt', this.addPluginInstallPrompt.bind(this));
DeckyBackend.addEventListener(
'loader/add_multiple_plugins_install_prompt',
@@ -122,6 +120,28 @@ class PluginLoader extends Logger {
<DeckyStateContextProvider deckyState={this.deckyState}>
<FaPlug />
<TabBadge />
<style>
{`
/* fixes random overscrolling in QAM */
.${quickAccessMenuClasses?.TabContentColumn} {
flex-grow: 1 !important;
margin-top: 0 !important;
margin-bottom: 0 !important;
justify-content: center !important;
}
.${quickAccessMenuClasses?.Tab} {
flex-grow: 1 !important;
height: unset !important;
--decky-qam-tab-max-height: 64px; /* make things a little easier for themers */
max-height: var(--decky-qam-tab-max-height) !important;
}
/* they broke the footer a while ago and forgot to update the styles LOL */
.${quickAccessMenuClasses?.Tabs}.${quickAccessMenuClasses.TabsWithFooter} {
margin-bottom: 0 !important;
padding-bottom: 0 !important;
}
`}
</style>
</DeckyStateContextProvider>
),
});
@@ -177,7 +197,7 @@ class PluginLoader extends Logger {
private getPluginsFromBackend = DeckyBackend.callable<
[],
{ name: string; version: string; load_type: PluginLoadType; disabled: boolean }[]
{ name: string; version: string; load_type: PluginLoadType }[]
>('loader/get_plugins');
private restartWebhelper = DeckyBackend.callable<[], void>('utilities/restart_webhelper');
@@ -200,16 +220,10 @@ class PluginLoader extends Logger {
this.runCrashChecker();
const plugins = await this.getPluginsFromBackend();
const pluginLoadPromises = [];
const disabledPlugins: DisabledPlugin[] = [];
const loadStart = performance.now();
for (const plugin of plugins) {
if (plugin.disabled) {
disabledPlugins.push({ name: plugin.name, version: plugin.version });
this.deckyState.setDisabledPlugins(disabledPlugins);
} else {
if (!this.hasPlugin(plugin.name))
pluginLoadPromises.push(this.importPlugin(plugin.name, plugin.version, plugin.load_type, false));
}
if (!this.hasPlugin(plugin.name))
pluginLoadPromises.push(this.importPlugin(plugin.name, plugin.version, plugin.load_type, false));
}
await Promise.all(pluginLoadPromises);
const loadEnd = performance.now();
@@ -260,9 +274,7 @@ class PluginLoader extends Logger {
public async checkPluginUpdates() {
const frozenPlugins = this.deckyState.publicState().frozenPlugins;
const updates = await checkForPluginUpdates(
this.deckyState.publicState().installedPlugins.filter((p) => !frozenPlugins.includes(p.name)),
);
const updates = await checkForPluginUpdates(this.plugins.filter((p) => !frozenPlugins.includes(p.name)));
this.deckyState.setUpdates(updates);
return updates;
}
@@ -300,7 +312,6 @@ class PluginLoader extends Logger {
version={version}
hash={hash}
installType={install_type}
disabled={this.deckyState.publicState().disabledPlugins.some((p) => p.name === artifact)}
onOK={() => DeckyBackend.call<[string]>('utilities/confirm_plugin_install', request_id)}
onCancel={() => DeckyBackend.call<[string]>('utilities/cancel_plugin_install', request_id)}
/>,
@@ -314,7 +325,6 @@ class PluginLoader extends Logger {
showModal(
<MultiplePluginsInstallModal
requests={requests}
disabledPlugins={this.deckyState.publicState().disabledPlugins}
onOK={() => DeckyBackend.call<[string]>('utilities/confirm_plugin_install', request_id)}
onCancel={() => DeckyBackend.call<[string]>('utilities/cancel_plugin_install', request_id)}
/>,
@@ -322,19 +332,7 @@ class PluginLoader extends Logger {
}
public uninstallPlugin(name: string, title: string, buttonText: string, description: string) {
showModal(
<PluginUninstallModal
name={name}
title={title}
buttonText={buttonText}
description={description}
deckyState={this.deckyState}
/>,
);
}
public disablePlugin(name: string, title: string, buttonText: string, description: string) {
showModal(<PluginDisableModal name={name} title={title} buttonText={buttonText} description={description} />);
showModal(<PluginUninstallModal name={name} title={title} buttonText={buttonText} description={description} />);
}
public hasPlugin(name: string) {
@@ -375,19 +373,6 @@ class PluginLoader extends Logger {
this.errorBoundaryHook.deinit();
}
public doDisablePlugin(name: string) {
const plugin = this.plugins.find((plugin) => plugin.name === name);
if (plugin == undefined) return;
plugin?.onDismount?.();
this.plugins = this.plugins.filter((p) => p !== plugin);
this.deckyState.setDisabledPlugins([
...this.deckyState.publicState().disabledPlugins,
{ name: plugin.name, version: plugin.version },
]);
this.deckyState.setPlugins(this.plugins);
}
public unloadPlugin(name: string, skipStateUpdate: boolean = false) {
const plugin = this.plugins.find((plugin) => plugin.name === name);
plugin?.onDismount?.();
@@ -400,7 +385,6 @@ class PluginLoader extends Logger {
version?: string | undefined,
loadType: PluginLoadType = PluginLoadType.ESMODULE_V1,
useQueue: boolean = true,
timeoutMS?: number,
) {
if (useQueue && this.reloadLock) {
this.log('Reload currently in progress, adding to queue', name);
@@ -414,11 +398,9 @@ class PluginLoader extends Logger {
this.unloadPlugin(name, true);
const startTime = performance.now();
await this.importReactPlugin(name, version, loadType, timeoutMS);
await this.importReactPlugin(name, version, loadType);
const endTime = performance.now();
this.deckyState.setDisabledPlugins(this.deckyState.publicState().disabledPlugins.filter((d) => d.name !== name));
this.deckyState.setPlugins(this.plugins);
this.log(`Loaded ${name} in ${endTime - startTime}ms`);
} catch (e) {
@@ -428,7 +410,7 @@ class PluginLoader extends Logger {
this.reloadLock = false;
const nextPlugin = this.pluginReloadQueue.shift();
if (nextPlugin) {
this.importPlugin(nextPlugin.name, nextPlugin.version, nextPlugin.loadType, true, timeoutMS);
this.importPlugin(nextPlugin.name, nextPlugin.version, loadType);
}
}
}
@@ -438,28 +420,12 @@ class PluginLoader extends Logger {
name: string,
version?: string,
loadType: PluginLoadType = PluginLoadType.ESMODULE_V1,
timeoutMS?: number,
) {
let spExists = this.checkForSP();
const timeoutException = new Error(
`${name} failed to load within ${timeoutMS ? `${timeoutMS / 1000} second` : ''} time limit`,
);
let timeout: number | undefined;
try {
switch (loadType) {
case PluginLoadType.ESMODULE_V1:
const importJS = () => import(`http://127.0.0.1:1337/plugins/${name}/dist/index.js?t=${Date.now()}`);
const promise =
timeoutMS === undefined
? importJS()
: Promise.race([
importJS(),
new Promise((_, reject) => (timeout = setTimeout(() => reject(timeoutException), timeoutMS))),
]);
const plugin_exports = await promise;
const plugin_exports = await import(`http://127.0.0.1:1337/plugins/${name}/dist/index.js?t=${Date.now()}`);
let plugin = plugin_exports.default();
this.plugins.push({
@@ -471,26 +437,12 @@ class PluginLoader extends Logger {
break;
case PluginLoadType.LEGACY_EVAL_IIFE:
const fetchJS = async () => {
const controller = new AbortController();
const { signal } = controller;
if (timeoutMS !== undefined) timeout = setTimeout(() => controller.abort(), timeoutMS);
try {
return await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, {
credentials: 'include',
headers: {
'X-Decky-Auth': deckyAuthToken,
},
signal,
});
} catch (e: any) {
throw 'name' in e && e.name === 'AbortError' ? timeoutException : e;
}
};
let res = await fetchJS();
let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, {
credentials: 'include',
headers: {
'X-Decky-Auth': deckyAuthToken,
},
});
if (res.ok) {
let plugin_export: (serverAPI: any) => Plugin = await eval(
(await res.text()) + `\n//# sourceURL=decky://decky/legacy_plugin/${encodeURIComponent(name)}/index.js`,
@@ -509,8 +461,6 @@ class PluginLoader extends Logger {
throw new Error(`${name} has no defined loadType.`);
}
} catch (e) {
if (e === timeoutException) throw timeoutException;
this.error('Error loading plugin ' + name, e);
const TheError: FC<{}> = () => (
<PanelSection>
@@ -553,8 +503,6 @@ class PluginLoader extends Logger {
body: '' + e,
icon: <FaExclamationCircle />,
});
} finally {
if (timeout !== undefined) clearTimeout(timeout);
}
if (spExists && !this.checkForSP()) {
-5
View File
@@ -1,4 +1,3 @@
import type { JSX } from 'react';
export enum PluginLoadType {
LEGACY_EVAL_IIFE = 0, // legacy, uses legacy serverAPI
ESMODULE_V1 = 1, // esmodule loading with modern @decky/backend apis
@@ -15,8 +14,6 @@ export interface Plugin {
titleView?: JSX.Element;
}
export type DisabledPlugin = Pick<Plugin, 'name' | 'version'>;
export enum InstallType {
INSTALL,
REINSTALL,
@@ -58,5 +55,3 @@ type installPluginsArgs = [
export let installPlugins = DeckyBackend.callable<installPluginsArgs>('utilities/install_plugins');
export let uninstallPlugin = DeckyBackend.callable<[name: string]>('utilities/uninstall_plugin');
export let enablePlugin = DeckyBackend.callable<[name: string]>('utilities/enable_plugin');
export let disablePlugin = DeckyBackend.callable<[name: string]>('utilities/disable_plugin');
+5 -5
View File
@@ -9,7 +9,7 @@ import {
getReactRoot,
sleep,
} from '@decky/ui';
import { FC, JSX, ReactElement, ReactNode, cloneElement, createElement } from 'react';
import { FC, ReactElement, ReactNode, cloneElement, createElement } from 'react';
import type { Route } from 'react-router';
import {
@@ -37,7 +37,7 @@ const isPatched = Symbol('is patched');
class RouterHook extends Logger {
private routerState: DeckyRouterState = new DeckyRouterState();
private globalComponentsState: DeckyGlobalComponentsState = new DeckyGlobalComponentsState();
private renderedComponents: ReactElement<any>[] = [];
private renderedComponents: ReactElement[] = [];
private Route: any;
private DeckyGamepadRouterWrapper = this.gamepadRouterWrapper.bind(this);
private DeckyDesktopRouterWrapper = this.desktopRouterWrapper.bind(this);
@@ -233,7 +233,7 @@ class RouterHook extends Logger {
return <>{this.renderedComponents}</>;
}
private gamepadRouterWrapper({ children }: { children: ReactElement<any> }) {
private gamepadRouterWrapper({ children }: { children: ReactElement }) {
// Used to store the new replicated routes we create to allow routes to be unpatched.
const { routes, routePatches } = useDeckyRouterState();
@@ -251,7 +251,7 @@ class RouterHook extends Logger {
return children;
}
private desktopRouterWrapper({ children }: { children: ReactElement<any> }) {
private desktopRouterWrapper({ children }: { children: ReactElement }) {
// Used to store the new replicated routes we create to allow routes to be unpatched.
this.debug('desktop router wrapper render', children);
const { routes, routePatches } = useDeckyRouterState();
@@ -287,7 +287,7 @@ class RouterHook extends Logger {
if (routes) {
if (!routeList[routerIndex - 1]?.length || routeList[routerIndex - 1]?.length !== routes.size) {
if (routeList[routerIndex - 1]?.length && routeList[routerIndex - 1].length !== routes.size) routerIndex--;
const newRouterArray: (ReactElement<any> | JSX.Element)[] = [];
const newRouterArray: (ReactElement | JSX.Element)[] = [];
routes.forEach(({ component, props }, path) => {
newRouterArray.push(
<Route path={path} {...props}>
+4 -7
View File
@@ -1,6 +1,6 @@
import { compare, validate } from 'compare-versions';
import { compare } from 'compare-versions';
import { DisabledPlugin, InstallType, Plugin, installPlugin, installPlugins } from './plugin';
import { InstallType, Plugin, installPlugin, installPlugins } from './plugin';
import { getSetting, setSetting } from './utils/settings';
export enum Store {
@@ -113,21 +113,18 @@ export async function requestMultiplePluginInstalls(requests: PluginInstallReque
);
}
export async function checkForPluginUpdates(plugins: (Plugin | DisabledPlugin)[]): Promise<PluginUpdateMapping> {
export async function checkForPluginUpdates(plugins: Plugin[]): Promise<PluginUpdateMapping> {
const serverData = await getPluginList();
const updateMap = new Map<string, StorePluginVersion>();
for (let plugin of plugins) {
const remotePlugin = serverData?.find((x) => x.name == plugin.name);
//FIXME: Ugly hack since plugin.version might be null during evaluation,
//so this will set the older version possible
const curVer = plugin.version ? plugin.version : '0.0.0';
const curVer = plugin.version ? plugin.version : '0.0';
if (
remotePlugin &&
remotePlugin.versions?.length > 0 &&
plugin.version != remotePlugin?.versions?.[0]?.name &&
validate(remotePlugin.versions?.[0]?.name) &&
validate(curVer) &&
compare(remotePlugin?.versions?.[0]?.name, curVer, '>')
) {
updateMap.set(plugin.name, remotePlugin.versions[0]);
+2 -2
View File
@@ -1,6 +1,6 @@
import { ErrorInfo } from 'react';
const pluginErrorRegex = /http:\/\/(?:localhost|127\.0\.0\.1):1337\/plugins\/([^\/]*)\//;
const pluginErrorRegex = /http:\/\/localhost:1337\/plugins\/([^\/]*)\//;
const pluginSourceMapErrorRegex = /decky:\/\/decky\/plugin\/([^\/]*)\//;
const legacyPluginErrorRegex = /decky:\/\/decky\/legacy_plugin\/([^\/]*)\/index.js/;
@@ -44,7 +44,7 @@ export function getLikelyErrorSource(error?: string): ErrorSource {
return [decodeURIComponent(legacyPluginMatch[1]), true, false];
}
if (error?.includes('http://localhost:1337/') || error?.includes('http://127.0.0.1:1337/')) {
if (error?.includes('http://localhost:1337/')) {
return ['the Decky frontend', false, false];
}
return ['Steam', false, true];
+4 -3
View File
@@ -2,7 +2,9 @@
"compilerOptions": {
"module": "ESNext",
"target": "ES2021",
"jsx": "react-jsx",
"jsx": "react",
"jsxFactory": "window.SP_REACT.createElement",
"jsxFragmentFactory": "window.SP_REACT.Fragment",
"declaration": false,
"moduleResolution": "node",
"noUnusedLocals": true,
@@ -13,8 +15,7 @@
"noImplicitAny": true,
"strict": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"skipLibCheck": true
"resolveJsonModule": true
},
"include": ["src", "index.d.ts"],
"exclude": ["node_modules"]
+5 -12
View File
@@ -20,10 +20,7 @@ if [ -z "$INSIDE_NIX_RANDOMSTRING" ] && command -v nix &> /dev/null; then
exit $?
fi
[[ -f "$HOME/.config/deckdebug/config.sh" ]] && source "$HOME/.config/deckdebug/config.sh"
CHROMIUM="${CHROMIUM:-chromium}"
required_dependencies=(websocat jq curl $CHROMIUM)
required_dependencies=(websocat jq curl chromium)
# Check if the dependencies are installed
for cmd in "${required_dependencies[@]}"; do
@@ -33,7 +30,7 @@ for cmd in "${required_dependencies[@]}"; do
fi
done
$CHROMIUM --remote-debugging-port=9222 &
chromium --remote-debugging-port=9222 &
sleep 2
ADDR=$1
@@ -52,14 +49,10 @@ while :; do
TARGET=$NEWTARGET
TARGETURL="http://$ADDR/devtools/inspector.html?ws=$ADDR/devtools/page/$TARGET"
echo '{"id": 1, "method": "Target.createTarget", "params": {"background": true, "url": "'$TARGETURL'"}}
LOCALTARGET=$(echo '{"id": 1, "method": "Target.createTarget", "params": {"background": true, "url": "'$TARGETURL'"}}
{"id": 2, "method": "Target.closeTarget", "params": {"targetId": "'$LOCALTARGET'"}}' \
| websocat -t ws://$LOCAL/devtools/page/$LOCALTARGET
sleep 2
LOCALTARGETS=$(curl -s http://$LOCAL/json/list)
LOCALTARGET=$(jq -r '.[] | select(.title | startswith("DevTools")) | .id' <<< "$LOCALTARGETS")
| websocat ws://$LOCAL/devtools/page/$LOCALTARGET \
| jq -r '.result.targetId')
echo started devtools at $LOCALTARGET
fi