Compare commits

..

11 Commits

Author SHA1 Message Date
AAGaming 259d01d7ec Remove incorrect padding in the decky menu (#891) 2026-03-21 21:09:13 -04:00
AAGaming a13887a13a fix(deps): bump @decky/ui to fix tabs component on bta 2026-03-21 20:39:27 -04:00
AAGaming b97c27aac4 Fixes for march 19th 2026 beta (#890) 2026-03-20 22:08:44 -04:00
EMERALD0874 8b8a1cc4d8 Removing FUNDING.yml 2026-02-08 15:29:28 -06:00
EMERALD0874 7a283c7608 Funding update - README, FUINDING.yml 2026-02-08 15:27:07 -06:00
jbofill 9f586a1b97 Feat: Disable plugins (#850)
* implement base frontend changes necessary for plugin disabling

* implement frontend diisable functions/ modal

* plugin disable boilerplate / untested

* Feat disable plugins (#810)

* implement base frontend changes necessary for plugin disabling

* implement frontend diisable functions/ modal

---------

Co-authored-by: Jesse Bofill <jesse_bofill@yahoo.com>

* fix mistakes

* add frontend

* working plugin disable, not tested extensively

* fix uninstalled hidden plugins remaining in list

* hide plugin irrelevant plugin setting menu option when disabled

* fix hidden plugin issues

* reset disabled plugin on uninstall

* fix plugin load on reenable

* move disable settings uninstall cleanup

* add engilsh tranlsations for enable/ disable elements

* fix bug where wrong loadType can get passed to importPlugin

* show correct number of hidden plugins if plugin is both hidden and disabled

* fix: get fresh list of plugin updates when changed in settings plugin list

* fix: fix invalid semver plugin version from preventing latest updates

* retain x position when changing focus in list items  that have multiple horizontal focusables

* correction to pluging version checking validation

* make sure disabled plugins get checked for updates

* show number of disabled plugins at bottom of plugin view

* add notice to update modals that disabled plugins will be enabled upon installation

* run formatter

* Update backend/decky_loader/locales/en-US.json

Co-authored-by: EMERALD <hudson.samuels@gmail.com>

* chore: correct filename typo

* chore: change disabled icon

* chore: revert accidental defsettings changes

* format

* add timeout to frontend importPlugin

if a request hangs this prevent it from blocking other plugin loads.
backend diaptch_plugin which calls this for individual plugin load (as opposed to batch) is set to 15s.
other callers of importPlugin are not using timeout, same as before.

* fix plugin update checking loop

---------

Co-authored-by: marios <marios8543@gmail.com>
Co-authored-by: EMERALD <hudson.samuels@gmail.com>
2025-12-30 13:29:08 -06:00
Sims 789851579b Fix settings import under windows (#858)
* test

* fix linting
2025-12-20 19:12:04 +00:00
AAGaming 7ea7bc7f9b fix(deps): bump @decky/ui to fix issues on beta (#853) 2025-11-26 22:10:33 -05:00
AAGaming e267ba9135 error regex update 2025-11-19 23:42:16 -05:00
AAGaming 44bb023b80 React 19 support (#818) 2025-10-15 00:31:12 -04:00
AAGaming 86b5567d4e dfl bump to fix DialogHeader component (#800) 2025-08-20 15:46:17 -04:00
34 changed files with 573 additions and 380 deletions
+14 -2
View File
@@ -3,7 +3,7 @@
<br> <br>
Decky Loader Decky Loader
<br> <br>
<a name="download button" href="https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/decky_installer.desktop"><img src="./docs/images/download_button.svg" alt="Download decky" width="350px" style="padding-top: 15px;"></a> <a name="download button" href="https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/decky_installer.desktop"><img src="./docs/images/download_button.svg" alt="Download decky" width="150px" style="padding-top: 15px;"></a>
</h1> </h1>
<p align="center"> <p align="center">
@@ -18,6 +18,15 @@
<!-- <img src="https://media.discordapp.net/attachments/966017112244125756/1012466063893610506/main.jpg" alt="Decky screenshot" width="80%">--> <!-- <img src="https://media.discordapp.net/attachments/966017112244125756/1012466063893610506/main.jpg" alt="Decky screenshot" width="80%">-->
</p> </p>
## 🩵 Backers and Sponsors
[Become a backer or sponsor](https://opencollective.com/steamdeckhomebrew) to support our work! Contributing to our collective effort will help Decky Loader developers cover the costs of web servers, acquire new development hardware, and more.
<!-- SPONSORS COMMENTED OUT UNTIL WE GET SOME SPONSORS TO AVOID BLANK SVG SPACE -->
<!-- <a href="https://opencollective.com/steamdeckhomebrew"><img alt="Steam Deck Homebrew sponsors on Open Collective" src="https://opencollective.com/steamdeckhomebrew/sponsors.svg?button=true&avatarHeight=46&width=750"></a> -->
<a href="https://opencollective.com/steamdeckhomebrew"><img alt="Steam Deck Homebrew backers on Open Collective" src="https://opencollective.com/steamdeckhomebrew/backers.svg?button=false&avatarHeight=46&width=750"></a>
## 📖 About ## 📖 About
Decky Loader is a homebrew plugin launcher for the Steam Deck. It can be used to [stylize your menus](https://github.com/suchmememanyskill/SDH-CssLoader), [change system sounds](https://github.com/EMERALD0874/SDH-AudioLoader), [adjust your screen saturation](https://github.com/libvibrant/vibrantDeck), [change additional system settings](https://github.com/NGnius/PowerTools), and [more](https://plugins.deckbrew.xyz/). Decky Loader is a homebrew plugin launcher for the Steam Deck. It can be used to [stylize your menus](https://github.com/suchmememanyskill/SDH-CssLoader), [change system sounds](https://github.com/EMERALD0874/SDH-AudioLoader), [adjust your screen saturation](https://github.com/libvibrant/vibrantDeck), [change additional system settings](https://github.com/NGnius/PowerTools), and [more](https://plugins.deckbrew.xyz/).
@@ -40,7 +49,9 @@ For more information about Decky Loader as well as documentation and development
- Sometimes Decky will disappear on SteamOS updates. This can easily be fixed by just re-running the installer and installing the stable branch again. If this doesn't work, try installing the prerelease instead. If that doesn't work, then [check the existing issues](https://github.com/SteamDeckHomebrew/decky-loader/issues) and if there isn't one then you can [file a new issue](https://github.com/SteamDeckHomebrew/decky-loader/issues/new?assignees=&labels=bug&template=bug_report.yml&title=%5BBUG%5D+%3Ctitle%3E). - Sometimes Decky will disappear on SteamOS updates. This can easily be fixed by just re-running the installer and installing the stable branch again. If this doesn't work, try installing the prerelease instead. If that doesn't work, then [check the existing issues](https://github.com/SteamDeckHomebrew/decky-loader/issues) and if there isn't one then you can [file a new issue](https://github.com/SteamDeckHomebrew/decky-loader/issues/new?assignees=&labels=bug&template=bug_report.yml&title=%5BBUG%5D+%3Ctitle%3E).
## 💾 Installation ## 💾 Installation
- This installation can be done without an admin/sudo password set. - This installation can be done without an admin/sudo password set.
1. Prepare a mouse and keyboard if possible. 1. Prepare a mouse and keyboard if possible.
- Keyboards and mice can be connected to the Steam Deck via USB-C or Bluetooth. - Keyboards and mice can be connected to the Steam Deck via USB-C or Bluetooth.
- Many Bluetooth keyboard and mouse apps are available for iOS and Android. (KDE connect is preinstalled on the steam deck) - Many Bluetooth keyboard and mouse apps are available for iOS and Android. (KDE connect is preinstalled on the steam deck)
@@ -54,7 +65,7 @@ For more information about Decky Loader as well as documentation and development
1. Either type your admin password or allow Decky to temporarily set your admin password to `Decky!` (this password will be removed after the installer finishes) 1. Either type your admin password or allow Decky to temporarily set your admin password to `Decky!` (this password will be removed after the installer finishes)
1. Choose the version of Decky Loader you want to install. 1. Choose the version of Decky Loader you want to install.
- **Latest Release** - **Latest Release**
Intended for most users. This is the latest stable version of Decky Loader. Intended for most users. This is the latest stable version of Decky Loader.
- **Latest Pre-Release** - **Latest Pre-Release**
Intended for plugin developers. Pre-releases are unlikely to be fully stable but contain the latest changes. For more information on plugin development, please consult [the wiki page](https://wiki.deckbrew.xyz/en/loader-dev/development). Intended for plugin developers. Pre-releases are unlikely to be fully stable but contain the latest changes. For more information on plugin development, please consult [the wiki page](https://wiki.deckbrew.xyz/en/loader-dev/development).
1. Open the Return to Gaming Mode shortcut on your desktop. 1. Open the Return to Gaming Mode shortcut on your desktop.
@@ -68,6 +79,7 @@ We are sorry to see you go! If you are considering uninstalling because you are
1. Press the <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16> button and open the Power menu. 1. Press the <img src="./docs/images/light/steam.svg#gh-dark-mode-only" height=16><img src="./docs/images/dark/steam.svg#gh-light-mode-only" height=16> button and open the Power menu.
1. Select "Switch to Desktop". 1. Select "Switch to Desktop".
1. Run the installer file again, and select `uninstall decky loader`. 1. Run the installer file again, and select `uninstall decky loader`.
- There is also a fast uninstall for those who can use Konsole. Run `curl -L https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/uninstall.sh | sh` and type your password when prompted. - There is also a fast uninstall for those who can use Konsole. Run `curl -L https://github.com/SteamDeckHomebrew/decky-installer/releases/latest/download/uninstall.sh | sh` and type your password when prompted.
## 🚀 Getting Started ## 🚀 Getting Started
+6
View File
@@ -150,6 +150,7 @@ class PluginBrowser:
# 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)
if name in self.plugins: if name in self.plugins:
logger.debug("Plugin %s was found", name) logger.debug("Plugin %s was found", name)
await self.plugins[name].stop(uninstall=True) await self.plugins[name].stop(uninstall=True)
@@ -345,5 +346,10 @@ class PluginBrowser:
if name in plugin_order: if name in plugin_order:
plugin_order.remove(name) plugin_order.remove(name)
self.settings.setSetting("pluginOrder", plugin_order) 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) logger.debug("Removed any settings for plugin %s", name)
+7 -2
View File
@@ -78,6 +78,7 @@ class Loader:
self.live_reload = live_reload self.live_reload = live_reload
self.reload_queue: ReloadQueue = Queue() self.reload_queue: ReloadQueue = Queue()
self.loop.create_task(self.handle_reloads()) self.loop.create_task(self.handle_reloads())
self.context: PluginManager = server_instance
if live_reload: if live_reload:
self.observer = Observer() self.observer = Observer()
@@ -130,7 +131,7 @@ 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, "load_type": i.load_type} for i in plugins] return [{"name": str(i), "version": i.version, "load_type": i.load_type, "disabled": i.disabled} for i in plugins]
async def handle_plugin_dist(self, request: web.Request): async def handle_plugin_dist(self, request: web.Request):
plugin = self.plugins[request.match_info["plugin_name"]] plugin = self.plugins[request.match_info["plugin_name"]]
@@ -164,6 +165,10 @@ class Loader:
await self.ws.emit(f"loader/plugin_event", {"plugin": plugin.name, "event": event, "args": args}) 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) 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 plugin.name in self.plugins:
if not "debug" in plugin.flags and refresh: 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") self.logger.info(f"Plugin {plugin.name} is already loaded and has requested to not be re-loaded")
@@ -183,7 +188,7 @@ class Loader:
print_exc() print_exc()
async def dispatch_plugin(self, name: str, version: str | None, load_type: int = PluginLoadType.ESMODULE_V1.value): 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) await self.ws.emit("loader/import_plugin", name, version, load_type, True, 15000)
async def import_plugins(self): async def import_plugins(self):
self.logger.info(f"import plugins from {self.plugin_path}") self.logger.info(f"import plugins from {self.plugin_path}")
+18 -3
View File
@@ -102,6 +102,7 @@
}, },
"no_hash": "This plugin does not have a hash, you are installing it at your own risk.", "no_hash": "This plugin does not have a hash, you are installing it at your own risk.",
"not_installed": "(not installed)", "not_installed": "(not installed)",
"disabled": "The plugin will be re-enabled after installation",
"overwrite": { "overwrite": {
"button_idle": "Overwrite", "button_idle": "Overwrite",
"button_processing": "Overwriting", "button_processing": "Overwriting",
@@ -133,10 +134,13 @@
"uninstall": "Uninstall", "uninstall": "Uninstall",
"update_all_one": "Update 1 plugin", "update_all_one": "Update 1 plugin",
"update_all_other": "Update {{count}} plugins", "update_all_other": "Update {{count}} plugins",
"update_to": "Update to {{name}}" "update_to": "Update to {{name}}",
"disable": "Disable",
"enable": "Enable"
}, },
"PluginListLabel": { "PluginListLabel": {
"hidden": "Hidden from the quick access menu" "hidden": "Hidden from the quick access menu",
"disabled": "Plugin disabled"
}, },
"PluginLoader": { "PluginLoader": {
"decky_title": "Decky", "decky_title": "Decky",
@@ -152,12 +156,23 @@
"desc": "Are you sure you want to uninstall {{name}}?", "desc": "Are you sure you want to uninstall {{name}}?",
"title": "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_one": "Updates available for 1 plugin!",
"plugin_update_other": "Updates available for {{count}} plugins!" "plugin_update_other": "Updates available for {{count}} plugins!"
}, },
"PluginView": { "PluginView": {
"hidden_one": "1 plugin is hidden from this list", "hidden_one": "1 plugin is hidden from this list",
"hidden_other": "{{count}} plugins are 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"
}, },
"RemoteDebugging": { "RemoteDebugging": {
"remote_cef": { "remote_cef": {
+1
View File
@@ -41,6 +41,7 @@ class PluginWrapper:
self.author = json["author"] self.author = json["author"]
self.flags = json["flags"] self.flags = json["flags"]
self.api_version = json["api_version"] if "api_version" in json else 0 self.api_version = json["api_version"] if "api_version" in json else 0
self.disabled = False
self.passive = not path.isfile(self.file) self.passive = not path.isfile(self.file)
@@ -14,6 +14,7 @@ from ..localplatform.localsocket import LocalSocket
from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path, ON_LINUX from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path, ON_LINUX
from ..enums import UserType from ..enums import UserType
from .. import helpers from .. import helpers
from .. import settings # pyright: ignore [reportUnusedImport]
from typing import List, TypeVar, Any from typing import List, TypeVar, Any
+34 -4
View File
@@ -1,5 +1,5 @@
from __future__ import annotations from __future__ import annotations
from os import stat_result from os import path, stat_result
import uuid import uuid
from urllib.parse import unquote from urllib.parse import unquote
from json.decoder import JSONDecodeError from json.decoder import JSONDecodeError
@@ -8,7 +8,7 @@ import re
from traceback import format_exc from traceback import format_exc
from stat import FILE_ATTRIBUTE_HIDDEN # pyright: ignore [reportAttributeAccessIssue, reportUnknownVariableType] from stat import FILE_ATTRIBUTE_HIDDEN # pyright: ignore [reportAttributeAccessIssue, reportUnknownVariableType]
from asyncio import StreamReader, StreamWriter, start_server, gather, open_connection from asyncio import StreamReader, StreamWriter, sleep, start_server, gather, open_connection
from aiohttp import ClientSession, hdrs from aiohttp import ClientSession, hdrs
from aiohttp.web import Request, StreamResponse, Response, json_response, post from aiohttp.web import Request, StreamResponse, Response, json_response, post
from typing import TYPE_CHECKING, Callable, Coroutine, Dict, Any, List, TypedDict from typing import TYPE_CHECKING, Callable, Coroutine, Dict, Any, List, TypedDict
@@ -80,6 +80,8 @@ class Utilities:
context.ws.add_route("utilities/restart_webhelper", self.restart_webhelper) 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/close_cef_socket", self.close_cef_socket)
context.ws.add_route("utilities/_call_legacy_utility", self._call_legacy_utility) 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([ context.web_app.add_routes([
post("/methods/{method_name}", self._handle_legacy_server_method_call) post("/methods/{method_name}", self._handle_legacy_server_method_call)
@@ -214,7 +216,7 @@ class Utilities:
async def http_request_legacy(self, method: str, url: str, extra_opts: Any = {}, timeout: int | None = None): async def http_request_legacy(self, method: str, url: str, extra_opts: Any = {}, timeout: int | None = None):
async with ClientSession() as web: async with ClientSession() as web:
res = await web.request(method, url, ssl=helpers.get_ssl_context(), timeout=timeout, **extra_opts) res = await web.request(method, url, ssl=helpers.get_ssl_context(), timeout=timeout, **extra_opts) # type: ignore
text = await res.text() text = await res.text()
return { return {
"status": res.status, "status": res.status,
@@ -390,7 +392,6 @@ class Utilities:
"total": len(all), "total": len(all),
} }
# Based on https://stackoverflow.com/a/46422554/13174603 # Based on https://stackoverflow.com/a/46422554/13174603
def start_rdt_proxy(self, ip: str, port: int): def start_rdt_proxy(self, ip: str, port: int):
async def pipe(reader: StreamReader, writer: StreamWriter): async def pipe(reader: StreamReader, writer: StreamWriter):
@@ -474,3 +475,32 @@ class Utilities:
async def get_tab_id(self, name: str): async def get_tab_id(self, name: str):
return (await get_tab(name)).id 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)
+24 -159
View File
@@ -1,162 +1,27 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) --> <svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 176.36 38">
<!-- Generator: Adobe Illustrator 29.2.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 116) -->
<defs>
<style>
.st0 {
fill: #3fafa8;
}
<svg .st1 {
width="81.700577mm" fill: #fff;
height="24.334814mm" }
viewBox="0 0 81.700577 24.334814" </style>
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="download.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="3.659624"
inkscape:cx="115.44902"
inkscape:cy="59.295709"
inkscape:window-width="1827"
inkscape:window-height="1233"
inkscape:window-x="69"
inkscape:window-y="38"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient4494">
<stop
style="stop-color:#009fff;stop-opacity:1;"
offset="0"
id="stop4490" />
<stop
style="stop-color:#ff1965;stop-opacity:1;"
offset="0.79417855"
id="stop4498" />
<stop
style="stop-color:#b9b500;stop-opacity:1;"
offset="1"
id="stop4492" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4494"
id="linearGradient4496"
x1="49.131042"
y1="118.6573"
x2="150.29259"
y2="138.74957"
gradientUnits="userSpaceOnUse"
spreadMethod="pad"
gradientTransform="matrix(1.0500324,0,0,1,-1.6155884,24.621921)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4494"
id="linearGradient13802"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0500324,0,0,1,-1.6155884,24.621921)"
x1="49.131042"
y1="118.6573"
x2="150.29259"
y2="138.74957"
spreadMethod="pad" />
</defs> </defs>
<g <rect class="st0" x="0" y="0" width="176.36" height="38" rx="19" ry="19"/>
inkscape:label="Layer 1" <g>
inkscape:groupmode="layer" <path class="st1" d="M59.4,26.66v-15.77h4.92c2.76,0,4.85.63,6.25,1.9,1.4,1.27,2.11,3.2,2.11,5.79s-.76,4.47-2.29,5.92c-1.53,1.45-3.58,2.17-6.17,2.17h-4.82ZM62.01,13.13v11.28h2.09c1.83,0,3.25-.5,4.28-1.49,1.03-.99,1.54-2.43,1.54-4.31s-.49-3.21-1.46-4.12c-.98-.91-2.41-1.37-4.31-1.37h-2.13Z"/>
id="layer1" <path class="st1" d="M80.12,26.92c-1.78,0-3.2-.52-4.25-1.57-1.05-1.05-1.57-2.46-1.57-4.23,0-1.8.56-3.24,1.67-4.34s2.54-1.64,4.31-1.64,3.17.54,4.2,1.62c1.03,1.08,1.54,2.47,1.54,4.18s-.54,3.18-1.62,4.31c-1.08,1.13-2.51,1.69-4.28,1.69ZM80.22,24.84c1.01,0,1.8-.35,2.35-1.04.56-.69.84-1.63.84-2.81s-.28-2.05-.84-2.74c-.56-.69-1.34-1.04-2.35-1.04s-1.81.36-2.41,1.07c-.6.71-.9,1.64-.9,2.78s.3,2.11.89,2.78,1.4,1.01,2.42,1.01Z"/>
transform="translate(-64.149712,-136.3326)"> <path class="st1" d="M103.61,15.4l-3.32,11.26h-2.67l-2.02-7.33c-.05-.19-.09-.34-.11-.45-.02-.11-.05-.25-.08-.43h-.05c-.03.18-.06.32-.09.43s-.07.25-.12.41l-2.19,7.36h-2.64l-3.31-11.26h2.6l2.01,7.71c.04.13.07.27.09.41.02.14.05.31.08.49h.07c.04-.19.07-.36.1-.5.03-.14.07-.29.12-.43l2.29-7.68h2.43l2.05,7.72c.02.09.05.21.08.36.03.15.07.33.1.54h.08c.04-.21.07-.36.09-.47.02-.11.06-.25.1-.43l1.95-7.72h2.39Z"/>
<rect <path class="st1" d="M115.36,26.66h-2.55v-6.59c0-.93-.19-1.64-.56-2.13-.37-.49-.93-.73-1.66-.73-.8,0-1.45.29-1.95.86-.5.57-.75,1.29-.75,2.17v6.42h-2.56v-11.26h2.56v1.53h.04c.4-.57.91-1.01,1.55-1.33.63-.31,1.32-.47,2.06-.47,1.25,0,2.2.4,2.85,1.19.65.79.98,1.92.98,3.4v6.94Z"/>
style="mix-blend-mode:normal;fill:url(#linearGradient13802);fill-opacity:1;stroke:none;stroke-width:0.271121" <path class="st1" d="M118.22,26.66V9.98h2.56v16.67h-2.56Z"/>
id="rect111" <path class="st1" d="M128.95,26.92c-1.78,0-3.2-.52-4.25-1.57s-1.57-2.46-1.57-4.23c0-1.8.56-3.24,1.67-4.34,1.11-1.1,2.54-1.64,4.31-1.64s3.17.54,4.2,1.62c1.03,1.08,1.54,2.47,1.54,4.18s-.54,3.18-1.62,4.31c-1.08,1.13-2.51,1.69-4.28,1.69ZM129.05,24.84c1.01,0,1.8-.35,2.35-1.04.56-.69.84-1.63.84-2.81s-.28-2.05-.84-2.74c-.56-.69-1.34-1.04-2.35-1.04s-1.81.36-2.41,1.07c-.6.71-.9,1.64-.9,2.78s.3,2.11.89,2.78c.59.67,1.4,1.01,2.42,1.01Z"/>
width="81.700577" <path class="st1" d="M144.71,26.66h-2.48v-1.4h-.04c-.4.54-.88.96-1.45,1.24-.57.28-1.21.42-1.91.42-1.04,0-1.89-.3-2.56-.89-.66-.59-1-1.37-1-2.33,0-1.03.33-1.86,1-2.49.66-.63,1.62-1.01,2.85-1.15l3.12-.35v-.54c0-.7-.19-1.22-.58-1.57s-.9-.52-1.53-.52-1.15.14-1.57.42c-.43.28-.78.68-1.06,1.2l-1.91-.98c.38-.76.98-1.38,1.8-1.86s1.8-.73,2.93-.73c1.42,0,2.51.37,3.26,1.12.75.74,1.13,1.82,1.13,3.24v7.17ZM142.25,22.08v-.62l-2.72.3c-.62.07-1.08.24-1.36.52-.29.28-.43.65-.43,1.12s.16.86.49,1.16c.33.3.75.45,1.27.45.82,0,1.49-.28,1.99-.83s.76-1.25.76-2.09Z"/>
height="24.334814" <path class="st1" d="M155.4,25.1c-.41.6-.93,1.06-1.55,1.36-.62.31-1.33.46-2.12.46-1.51,0-2.7-.5-3.57-1.5-.87-1-1.3-2.38-1.3-4.13,0-1.89.49-3.39,1.46-4.5.97-1.11,2.27-1.66,3.89-1.66.7,0,1.34.14,1.91.42.57.28,1,.63,1.29,1.06h.04v-6.62h2.56v16.67h-2.56v-1.56h-.04ZM149.46,21.19c0,1.14.26,2.04.78,2.68.52.65,1.24.97,2.16.97s1.69-.32,2.24-.97c.56-.64.84-1.47.84-2.49v-1.29c0-.82-.27-1.51-.81-2.06s-1.24-.83-2.1-.83c-.96,0-1.72.34-2.28,1.03s-.84,1.67-.84,2.95Z"/>
x="64.149712"
y="136.3326"
ry="8.1781616" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#000000;stroke:none;stroke-width:0.264583"
x="66.364288"
y="124.84658"
id="text10382"><tspan
sodipodi:role="line"
id="tspan10380"
style="stroke-width:0.264583"
x="66.364288"
y="124.84658" /></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15.1694px;font-family:sans-serif;-inkscape-font-specification:sans-serif;white-space:pre;inline-size:82.6483;display:inline;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.264583"
x="67.732498"
y="126.05277"
id="text10440"
transform="translate(1.088576,28.135753)"><tspan
x="67.732498"
y="126.05277"
id="tspan13872">Download</tspan></text>
<rect
style="mix-blend-mode:normal;fill:url(#linearGradient4496);fill-opacity:1;stroke:none;stroke-width:0.271121"
id="rect13792"
width="81.700577"
height="24.334814"
x="64.149712"
y="136.3326"
ry="8.1781616" />
<text
xml:space="preserve"
style="font-size:3.175px;fill:#000000;stroke:none;stroke-width:0.264583"
x="66.364288"
y="124.84658"
id="text13796"><tspan
sodipodi:role="line"
id="tspan13794"
style="stroke-width:0.264583"
x="66.364288"
y="124.84658" /></text>
<g
aria-label="Download"
transform="translate(1.088576,28.135753)"
id="text13800"
style="font-size:15.1694px;-inkscape-font-specification:sans-serif;white-space:pre;inline-size:82.6483;display:inline;fill:#ffffff;stroke-width:0.264583">
<path
d="m 77.880751,120.53111 q 0,2.74566 -1.501771,4.14125 -1.486601,1.38041 -4.156416,1.38041 h -3.01871 v -10.83095 h 3.337268 q 1.638295,0 2.836678,0.60678 1.198382,0.60677 1.850666,1.78999 0.652285,1.16804 0.652285,2.91252 z m -1.441093,0.0455 q 0,-2.16923 -1.077028,-3.17041 -1.061858,-1.01635 -3.01871,-1.01635 H 70.5691 v 8.49487 h 1.471432 q 4.399126,0 4.399126,-4.30811 z"
id="path13828" />
<path
d="m 87.164417,121.9722 q 0,2.01753 -1.03152,3.1249 -1.01635,1.10737 -2.760831,1.10737 -1.077027,0 -1.926513,-0.48542 -0.834317,-0.50059 -1.319738,-1.4411 -0.485421,-0.95567 -0.485421,-2.30575 0,-2.01753 1.01635,-3.10972 1.01635,-1.0922 2.760831,-1.0922 1.107366,0 1.941683,0.50059 0.849486,0.48542 1.319738,1.42592 0.485421,0.92534 0.485421,2.27541 z m -6.143608,0 q 0,1.4411 0.561268,2.29058 0.576437,0.83432 1.820328,0.83432 1.228722,0 1.805159,-0.83432 0.576437,-0.84948 0.576437,-2.29058 0,-1.44109 -0.576437,-2.26024 -0.576437,-0.81914 -1.820328,-0.81914 -1.243891,0 -1.805159,0.81914 -0.561268,0.81915 -0.561268,2.26024 z"
id="path13830" />
<path
d="m 94.218174,121.45644 q -0.197202,-0.62194 -0.348896,-1.21355 -0.136525,-0.60677 -0.212372,-0.9405 h -0.06068 q -0.06068,0.33373 -0.197203,0.9405 -0.136524,0.59161 -0.348896,1.22872 l -1.456262,4.56599 h -1.51694 l -2.229902,-8.1308 h 1.380415 l 1.122536,4.33845 q 0.166863,0.65229 0.318557,1.31974 0.151694,0.66745 0.212372,1.10737 h 0.06068 q 0.06068,-0.25788 0.136525,-0.63712 0.09102,-0.37923 0.197202,-0.78881 0.106186,-0.42474 0.212372,-0.75847 l 1.441093,-4.58116 h 1.456262 l 1.395585,4.58116 q 0.166864,0.51576 0.318558,1.12254 0.166863,0.60678 0.227541,1.04669 h 0.06068 q 0.04551,-0.37924 0.197202,-1.04669 0.166864,-0.66745 0.348897,-1.36525 l 1.137705,-4.33845 h 1.365246 l -2.260241,8.1308 h -1.562448 z"
id="path13832" />
<path
d="m 104.8064,117.77028 q 1.45627,0 2.19957,0.71296 0.7433,0.69779 0.7433,2.27541 v 5.29412 h -1.31974 v -5.2031 q 0,-1.95685 -1.82033,-1.95685 -1.35007,0 -1.86583,0.75847 -0.51576,0.75847 -0.51576,2.18439 v 4.21709 h -1.33491 v -8.1308 h 1.07703 l 0.1972,1.10737 h 0.0759 q 0.3944,-0.63711 1.09219,-0.9405 0.69779,-0.31856 1.47143,-0.31856 z"
id="path13834" />
<path
d="m 111.6023,126.05277 h -1.33491 v -11.52874 h 1.33491 z"
id="path13836" />
<path
d="m 121.25003,121.9722 q 0,2.01753 -1.03152,3.1249 -1.01635,1.10737 -2.76084,1.10737 -1.07702,0 -1.92651,-0.48542 -0.83432,-0.50059 -1.31974,-1.4411 -0.48542,-0.95567 -0.48542,-2.30575 0,-2.01753 1.01635,-3.10972 1.01635,-1.0922 2.76083,-1.0922 1.10737,0 1.94169,0.50059 0.84948,0.48542 1.31973,1.42592 0.48543,0.92534 0.48543,2.27541 z m -6.14361,0 q 0,1.4411 0.56127,2.29058 0.57643,0.83432 1.82032,0.83432 1.22873,0 1.80516,-0.83432 0.57644,-0.84948 0.57644,-2.29058 0,-1.44109 -0.57644,-2.26024 -0.57643,-0.81914 -1.82033,-0.81914 -1.24389,0 -1.80515,0.81914 -0.56127,0.81915 -0.56127,2.26024 z"
id="path13838" />
<path
d="m 126.43796,117.78545 q 1.4866,0 2.19956,0.65228 0.71296,0.65229 0.71296,2.07821 v 5.53683 h -0.97084 l -0.25788,-1.15287 h -0.0607 q -0.53093,0.66745 -1.12253,0.98601 -0.57644,0.31856 -1.60796,0.31856 -1.10737,0 -1.8355,-0.57644 -0.72813,-0.59161 -0.72813,-1.8355 0,-1.21355 0.95567,-1.86583 0.95567,-0.66746 2.94287,-0.72814 l 1.38041,-0.0455 v -0.48542 q 0,-1.01635 -0.43991,-1.41076 -0.43991,-0.3944 -1.24389,-0.3944 -0.63712,0 -1.21355,0.1972 -0.57644,0.18203 -1.07703,0.42474 l -0.40957,-1.00118 q 0.53092,-0.28822 1.25906,-0.48542 0.72813,-0.21237 1.51694,-0.21237 z m 0.3944,4.33845 q -1.51694,0.0607 -2.10855,0.48542 -0.57643,0.42474 -0.57643,1.19838 0,0.68262 0.40957,1.00118 0.42474,0.31856 1.07703,0.31856 1.03152,0 1.71414,-0.56127 0.68262,-0.57644 0.68262,-1.75965 v -0.72813 z"
id="path13840" />
<path
d="m 134.7508,126.20447 q -1.51694,0 -2.42711,-1.04669 -0.91016,-1.06186 -0.91016,-3.15524 0,-2.09337 0.91016,-3.15523 0.92534,-1.07703 2.44228,-1.07703 0.9405,0 1.53211,0.3489 0.60677,0.34889 0.98601,0.84948 h 0.091 q -0.0152,-0.1972 -0.0607,-0.57643 -0.0303,-0.39441 -0.0303,-0.62195 v -3.24625 h 1.3349 v 11.52874 h -1.07702 l -0.19721,-1.09219 h -0.0607 q -0.36407,0.51576 -0.97084,0.87982 -0.60678,0.36407 -1.56245,0.36407 z m 0.21237,-1.10737 q 1.2894,0 1.80516,-0.69779 0.53093,-0.71296 0.53093,-2.13889 v -0.24271 q 0,-1.51694 -0.50059,-2.32092 -0.50059,-0.81914 -1.85067,-0.81914 -1.07703,0 -1.62313,0.86465 -0.53093,0.84949 -0.53093,2.29058 0,1.45626 0.53093,2.26024 0.5461,0.80398 1.6383,0.80398 z"
id="path13842" />
</g>
</g> </g>
</svg> <path class="st1" d="M29.96,6.28h3.98c.66,0,1.19.53,1.19,1.19v8.35h4.36c.88,0,1.33,1.07.7,1.69l-7.56,7.56c-.37.37-.98.37-1.36,0l-7.57-7.56c-.63-.63-.18-1.69.7-1.69h4.36V7.47c0-.66.53-1.19,1.19-1.19ZM44.67,24.96v5.57c0,.66-.53,1.19-1.19,1.19h-23.06c-.66,0-1.19-.53-1.19-1.19v-5.57c0-.66.53-1.19,1.19-1.19h7.29l2.44,2.44c1,1,2.61,1,3.61,0l2.44-2.44h7.29c.66,0,1.19.53,1.19,1.19ZM38.5,29.34c0-.55-.45-.99-.99-.99s-.99.45-.99.99.45.99.99.99.99-.45.99-.99ZM41.68,29.34c0-.55-.45-.99-.99-.99s-.99.45-.99.99.45.99.99.99.99-.45.99-.99Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

+9 -9
View File
@@ -13,15 +13,15 @@
"localize": "i18next" "localize": "i18next"
}, },
"devDependencies": { "devDependencies": {
"@decky/api": "^1.1.1", "@decky/api": "^1.1.3",
"@rollup/plugin-commonjs": "^26.0.1", "@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-image": "^3.0.3", "@rollup/plugin-image": "^3.0.3",
"@rollup/plugin-json": "^6.1.0", "@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.7", "@rollup/plugin-replace": "^5.0.7",
"@rollup/plugin-typescript": "^11.1.6", "@rollup/plugin-typescript": "^11.1.6",
"@types/react": "18.3.3", "@types/react": "19.1.1",
"@types/react-dom": "18.3.0", "@types/react-dom": "19.1.1",
"@types/react-file-icon": "^1.0.4", "@types/react-file-icon": "^1.0.4",
"@types/react-router": "5.1.20", "@types/react-router": "5.1.20",
"husky": "^9.0.11", "husky": "^9.0.11",
@@ -30,8 +30,8 @@
"inquirer": "^9.2.23", "inquirer": "^9.2.23",
"prettier": "^3.3.2", "prettier": "^3.3.2",
"prettier-plugin-import-sort": "^0.0.7", "prettier-plugin-import-sort": "^0.0.7",
"react": "18.3.1", "react": "19.1.1",
"react-dom": "18.3.1", "react-dom": "19.1.1",
"rollup": "^4.22.4", "rollup": "^4.22.4",
"rollup-plugin-delete": "^2.0.0", "rollup-plugin-delete": "^2.0.0",
"rollup-plugin-external-globals": "^0.10.0", "rollup-plugin-external-globals": "^0.10.0",
@@ -47,13 +47,13 @@
} }
}, },
"dependencies": { "dependencies": {
"@decky/ui": "^4.10.4", "@decky/ui": "^4.11.3",
"compare-versions": "^6.1.1", "compare-versions": "^6.1.1",
"filesize": "^10.1.2", "filesize": "^10.1.2",
"i18next": "^23.11.5", "i18next": "^25.6.0",
"i18next-http-backend": "^2.5.2", "i18next-http-backend": "^2.5.2",
"react-file-icon": "^1.5.0", "react-file-icon": "^1.6.0",
"react-i18next": "^14.1.2", "react-i18next": "^16.0.1",
"react-icons": "^5.2.1", "react-icons": "^5.2.1",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"remark-gfm": "^4.0.0" "remark-gfm": "^4.0.0"
+97 -82
View File
@@ -9,8 +9,8 @@ importers:
.: .:
dependencies: dependencies:
'@decky/ui': '@decky/ui':
specifier: ^4.10.4 specifier: ^4.11.3
version: 4.10.4 version: 4.11.3
compare-versions: compare-versions:
specifier: ^6.1.1 specifier: ^6.1.1
version: 6.1.1 version: 6.1.1
@@ -18,30 +18,30 @@ importers:
specifier: ^10.1.2 specifier: ^10.1.2
version: 10.1.2 version: 10.1.2
i18next: i18next:
specifier: ^23.11.5 specifier: ^25.6.0
version: 23.11.5 version: 25.6.0(typescript@5.4.5)
i18next-http-backend: i18next-http-backend:
specifier: ^2.5.2 specifier: ^2.5.2
version: 2.5.2 version: 2.5.2
react-file-icon: react-file-icon:
specifier: ^1.5.0 specifier: ^1.6.0
version: 1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 1.6.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
react-i18next: react-i18next:
specifier: ^14.1.2 specifier: ^16.0.1
version: 14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.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)
react-icons: react-icons:
specifier: ^5.2.1 specifier: ^5.2.1
version: 5.2.1(react@18.3.1) version: 5.2.1(react@19.1.1)
react-markdown: react-markdown:
specifier: ^9.0.1 specifier: ^9.0.1
version: 9.0.1(@types/react@18.3.3)(react@18.3.1) version: 9.0.1(@types/react@19.1.1)(react@19.1.1)
remark-gfm: remark-gfm:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0 version: 4.0.0
devDependencies: devDependencies:
'@decky/api': '@decky/api':
specifier: ^1.1.1 specifier: ^1.1.3
version: 1.1.1 version: 1.1.3
'@rollup/plugin-commonjs': '@rollup/plugin-commonjs':
specifier: ^26.0.1 specifier: ^26.0.1
version: 26.0.1(rollup@4.22.4) version: 26.0.1(rollup@4.22.4)
@@ -61,11 +61,11 @@ importers:
specifier: ^11.1.6 specifier: ^11.1.6
version: 11.1.6(rollup@4.22.4)(tslib@2.6.3)(typescript@5.4.5) version: 11.1.6(rollup@4.22.4)(tslib@2.6.3)(typescript@5.4.5)
'@types/react': '@types/react':
specifier: 18.3.3 specifier: 19.1.1
version: 18.3.3 version: 19.1.1
'@types/react-dom': '@types/react-dom':
specifier: 18.3.0 specifier: 19.1.1
version: 18.3.0 version: 19.1.1(@types/react@19.1.1)
'@types/react-file-icon': '@types/react-file-icon':
specifier: ^1.0.4 specifier: ^1.0.4
version: 1.0.4 version: 1.0.4
@@ -91,11 +91,11 @@ importers:
specifier: ^0.0.7 specifier: ^0.0.7
version: 0.0.7(prettier@3.3.2) version: 0.0.7(prettier@3.3.2)
react: react:
specifier: 18.3.1 specifier: 19.1.1
version: 18.3.1 version: 19.1.1
react-dom: react-dom:
specifier: 18.3.1 specifier: 19.1.1
version: 18.3.1(react@18.3.1) version: 19.1.1(react@19.1.1)
rollup: rollup:
specifier: ^4.22.4 specifier: ^4.22.4
version: 4.22.4 version: 4.22.4
@@ -203,6 +203,10 @@ packages:
resolution: {integrity: sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==} resolution: {integrity: sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==}
engines: {node: '>=6.9.0'} 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': '@babel/template@7.24.7':
resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==} resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -215,11 +219,11 @@ packages:
resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==} resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@decky/api@1.1.1': '@decky/api@1.1.3':
resolution: {integrity: sha512-R5fkBRHBt5QIQY7Q0AlbVIhlIZ/nTzwBOoi8Rt4Go2fjFnoMKPInCJl6cPjXzimGwl2pyqKJgY6VnH6ar0XrHQ==} resolution: {integrity: sha512-XsPCZxfxk5I1UtylIUN3qaWQI31siQbKfbLIskkI5innEatY1m4NQqBv/6hwPaO9mKMbdqYpnh5PSJDeMEOOBA==}
'@decky/ui@4.10.4': '@decky/ui@4.11.3':
resolution: {integrity: sha512-swgC4IVtQzZVw8dtP/iztpNYUl1eR0dxWfiMpswY8YglDsBn4ntspbL91Ic4WgxvkOEMSpsIs+zkVtjHE9zi3A==} resolution: {integrity: sha512-lA79I3isehiYpjisbrQCiqmzOOVjJaMwd8Ta3ZHVH3iNgCgeeG9F4LjkjiV3Palpx/ShDawR9H4MOsvhd/EfxA==}
'@esbuild/aix-ppc64@0.20.2': '@esbuild/aix-ppc64@0.20.2':
resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==}
@@ -598,11 +602,10 @@ packages:
'@types/node@20.14.2': '@types/node@20.14.2':
resolution: {integrity: sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==} resolution: {integrity: sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==}
'@types/prop-types@15.7.12': '@types/react-dom@19.1.1':
resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} resolution: {integrity: sha512-jFf/woGTVTjUJsl2O7hcopJ1r0upqoq/vIOoCj0yLh3RIXxWcljlpuZ+vEBRXsymD1jhfeJrlyTy/S1UW+4y1w==}
peerDependencies:
'@types/react-dom@18.3.0': '@types/react': ^19.0.0
resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==}
'@types/react-file-icon@1.0.4': '@types/react-file-icon@1.0.4':
resolution: {integrity: sha512-c1mIklUDaxm9odxf8RTiy/EAxsblZliJ86EKIOAyuafP9eK3iudyn4ATv53DX6ZvgGymc7IttVNm97LTGnTiYA==} resolution: {integrity: sha512-c1mIklUDaxm9odxf8RTiy/EAxsblZliJ86EKIOAyuafP9eK3iudyn4ATv53DX6ZvgGymc7IttVNm97LTGnTiYA==}
@@ -610,8 +613,8 @@ packages:
'@types/react-router@5.1.20': '@types/react-router@5.1.20':
resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==}
'@types/react@18.3.3': '@types/react@19.1.1':
resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==} resolution: {integrity: sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==}
'@types/resolve@1.20.2': '@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
@@ -1187,8 +1190,16 @@ packages:
engines: {node: '>=18.0.0 || >=20.0.0 || >=22.0.0', npm: '>=6', yarn: '>=1'} engines: {node: '>=18.0.0 || >=20.0.0 || >=22.0.0', npm: '>=6', yarn: '>=1'}
hasBin: true hasBin: true
i18next@23.11.5: i18next@23.16.8:
resolution: {integrity: sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==} 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
iconv-lite@0.4.24: iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
@@ -1709,29 +1720,32 @@ packages:
quick-temp@0.1.8: quick-temp@0.1.8:
resolution: {integrity: sha512-YsmIFfD9j2zaFwJkzI6eMG7y0lQP7YeWzgtFgNl38pGWZBSXJooZbOWwkcRot7Vt0Fg9L23pX0tqWU3VvLDsiA==} resolution: {integrity: sha512-YsmIFfD9j2zaFwJkzI6eMG7y0lQP7YeWzgtFgNl38pGWZBSXJooZbOWwkcRot7Vt0Fg9L23pX0tqWU3VvLDsiA==}
react-dom@18.3.1: react-dom@19.1.1:
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==}
peerDependencies: peerDependencies:
react: ^18.3.1 react: ^19.1.1
react-file-icon@1.5.0: react-file-icon@1.6.0:
resolution: {integrity: sha512-6K2/nAI69CS838HOS+4S95MLXwf1neWywek1FgqcTFPTYjnM8XT7aBLz4gkjoqQKY9qPhu3A2tu+lvxhmZYY9w==} resolution: {integrity: sha512-Ba4Qa2ya/kvhcCd4LJja77sV7JD7u1ZXcI1DUz+TII3nGmglG6QY+NZeHizThokgct3qI0glwb9eV8NqRGs5lw==}
peerDependencies: peerDependencies:
react: ^18.0.0 || ^17.0.0 || ^16.2.0 react: ^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.2.0
react-dom: ^18.0.0 || ^17.0.0 || ^16.2.0 react-dom: ^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.2.0
react-i18next@14.1.2: react-i18next@16.0.1:
resolution: {integrity: sha512-FSIcJy6oauJbGEXfhUgVeLzvWBhIBIS+/9c6Lj4niwKZyGaGb4V4vUbATXSlsHJDXXB+ociNxqFNiFuV1gmoqg==} resolution: {integrity: sha512-0S//bpYEkCPjzuVmxDf9Z6+Y+ArNvpAUk7eDL4qNCZXjDh6Z9j6MZ+NThU7kMCOsmYmDCun3GYEwkiOjjZo9Ug==}
peerDependencies: peerDependencies:
i18next: '>= 23.2.3' i18next: '>= 25.5.2'
react: '>= 16.8.0' react: '>= 16.8.0'
react-dom: '*' react-dom: '*'
react-native: '*' react-native: '*'
typescript: ^5
peerDependenciesMeta: peerDependenciesMeta:
react-dom: react-dom:
optional: true optional: true
react-native: react-native:
optional: true optional: true
typescript:
optional: true
react-icons@5.2.1: react-icons@5.2.1:
resolution: {integrity: sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==} resolution: {integrity: sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==}
@@ -1747,8 +1761,8 @@ packages:
'@types/react': '>=18' '@types/react': '>=18'
react: '>=18' react: '>=18'
react@18.3.1: react@19.1.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
readable-stream@2.3.8: readable-stream@2.3.8:
@@ -1873,8 +1887,8 @@ packages:
safer-buffer@2.1.2: safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
scheduler@0.23.2: scheduler@0.26.0:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
semver@6.3.1: semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
@@ -2266,6 +2280,8 @@ snapshots:
dependencies: dependencies:
regenerator-runtime: 0.14.1 regenerator-runtime: 0.14.1
'@babel/runtime@7.28.4': {}
'@babel/template@7.24.7': '@babel/template@7.24.7':
dependencies: dependencies:
'@babel/code-frame': 7.24.7 '@babel/code-frame': 7.24.7
@@ -2293,9 +2309,9 @@ snapshots:
'@babel/helper-validator-identifier': 7.24.7 '@babel/helper-validator-identifier': 7.24.7
to-fast-properties: 2.0.0 to-fast-properties: 2.0.0
'@decky/api@1.1.1': {} '@decky/api@1.1.3': {}
'@decky/ui@4.10.4': {} '@decky/ui@4.11.3': {}
'@esbuild/aix-ppc64@0.20.2': '@esbuild/aix-ppc64@0.20.2':
optional: true optional: true
@@ -2567,24 +2583,21 @@ snapshots:
dependencies: dependencies:
undici-types: 5.26.5 undici-types: 5.26.5
'@types/prop-types@15.7.12': {} '@types/react-dom@19.1.1(@types/react@19.1.1)':
'@types/react-dom@18.3.0':
dependencies: dependencies:
'@types/react': 18.3.3 '@types/react': 19.1.1
'@types/react-file-icon@1.0.4': '@types/react-file-icon@1.0.4':
dependencies: dependencies:
'@types/react': 18.3.3 '@types/react': 19.1.1
'@types/react-router@5.1.20': '@types/react-router@5.1.20':
dependencies: dependencies:
'@types/history': 4.7.11 '@types/history': 4.7.11
'@types/react': 18.3.3 '@types/react': 19.1.1
'@types/react@18.3.3': '@types/react@19.1.1':
dependencies: dependencies:
'@types/prop-types': 15.7.12
csstype: 3.1.3 csstype: 3.1.3
'@types/resolve@1.20.2': {} '@types/resolve@1.20.2': {}
@@ -3229,7 +3242,7 @@ snapshots:
esbuild: 0.20.2 esbuild: 0.20.2
fs-extra: 11.2.0 fs-extra: 11.2.0
gulp-sort: 2.0.0 gulp-sort: 2.0.0
i18next: 23.11.5 i18next: 23.16.8
js-yaml: 4.1.0 js-yaml: 4.1.0
lilconfig: 3.1.2 lilconfig: 3.1.2
rsvp: 4.8.5 rsvp: 4.8.5
@@ -3240,10 +3253,16 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
i18next@23.11.5: i18next@23.16.8:
dependencies: dependencies:
'@babel/runtime': 7.24.7 '@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: iconv-lite@0.4.24:
dependencies: dependencies:
safer-buffer: 2.1.2 safer-buffer: 2.1.2
@@ -3960,43 +3979,43 @@ snapshots:
rimraf: 2.7.1 rimraf: 2.7.1
underscore.string: 3.3.6 underscore.string: 3.3.6
react-dom@18.3.1(react@18.3.1): react-dom@19.1.1(react@19.1.1):
dependencies: dependencies:
loose-envify: 1.4.0 react: 19.1.1
react: 18.3.1 scheduler: 0.26.0
scheduler: 0.23.2
react-file-icon@1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): react-file-icon@1.6.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
dependencies: dependencies:
colord: 2.9.3 colord: 2.9.3
prop-types: 15.8.1 prop-types: 15.8.1
react: 18.3.1 react: 19.1.1
react-dom: 18.3.1(react@18.3.1) react-dom: 19.1.1(react@19.1.1)
react-i18next@14.1.2(i18next@23.11.5)(react-dom@18.3.1(react@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):
dependencies: dependencies:
'@babel/runtime': 7.24.7 '@babel/runtime': 7.28.4
html-parse-stringify: 3.0.1 html-parse-stringify: 3.0.1
i18next: 23.11.5 i18next: 25.6.0(typescript@5.4.5)
react: 18.3.1 react: 19.1.1
optionalDependencies: optionalDependencies:
react-dom: 18.3.1(react@18.3.1) react-dom: 19.1.1(react@19.1.1)
typescript: 5.4.5
react-icons@5.2.1(react@18.3.1): react-icons@5.2.1(react@19.1.1):
dependencies: dependencies:
react: 18.3.1 react: 19.1.1
react-is@16.13.1: {} react-is@16.13.1: {}
react-markdown@9.0.1(@types/react@18.3.3)(react@18.3.1): react-markdown@9.0.1(@types/react@19.1.1)(react@19.1.1):
dependencies: dependencies:
'@types/hast': 3.0.4 '@types/hast': 3.0.4
'@types/react': 18.3.3 '@types/react': 19.1.1
devlop: 1.1.0 devlop: 1.1.0
hast-util-to-jsx-runtime: 2.3.0 hast-util-to-jsx-runtime: 2.3.0
html-url-attributes: 3.0.0 html-url-attributes: 3.0.0
mdast-util-to-hast: 13.2.0 mdast-util-to-hast: 13.2.0
react: 18.3.1 react: 19.1.1
remark-parse: 11.0.0 remark-parse: 11.0.0
remark-rehype: 11.1.0 remark-rehype: 11.1.0
unified: 11.0.4 unified: 11.0.4
@@ -4005,9 +4024,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
react@18.3.1: react@19.1.1: {}
dependencies:
loose-envify: 1.4.0
readable-stream@2.3.8: readable-stream@2.3.8:
dependencies: dependencies:
@@ -4164,9 +4181,7 @@ snapshots:
safer-buffer@2.1.2: {} safer-buffer@2.1.2: {}
scheduler@0.23.2: scheduler@0.26.0: {}
dependencies:
loose-envify: 1.4.0
semver@6.3.1: {} semver@6.3.1: {}
+1
View File
@@ -23,6 +23,7 @@ export default defineConfig([
}), }),
externalGlobals({ externalGlobals({
react: 'SP_REACT', react: 'SP_REACT',
'react/jsx-runtime': 'SP_JSX',
'react-dom': 'SP_REACTDOM', 'react-dom': 'SP_REACTDOM',
// hack to shut up react-markdown // hack to shut up react-markdown
process: '{cwd: () => {}}', process: '{cwd: () => {}}',
+17 -1
View File
@@ -1,12 +1,14 @@
import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react'; import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
import { DEFAULT_NOTIFICATION_SETTINGS, NotificationSettings } from '../notification-service'; import { DEFAULT_NOTIFICATION_SETTINGS, NotificationSettings } from '../notification-service';
import { Plugin } from '../plugin'; import { DisabledPlugin, Plugin } from '../plugin';
import { PluginUpdateMapping } from '../store'; import { PluginUpdateMapping } from '../store';
import { VerInfo } from '../updater'; import { VerInfo } from '../updater';
interface PublicDeckyState { interface PublicDeckyState {
plugins: Plugin[]; plugins: Plugin[];
disabledPlugins: DisabledPlugin[];
installedPlugins: (Plugin | DisabledPlugin)[];
pluginOrder: string[]; pluginOrder: string[];
frozenPlugins: string[]; frozenPlugins: string[];
hiddenPlugins: string[]; hiddenPlugins: string[];
@@ -26,6 +28,8 @@ export interface UserInfo {
export class DeckyState { export class DeckyState {
private _plugins: Plugin[] = []; private _plugins: Plugin[] = [];
private _disabledPlugins: DisabledPlugin[] = [];
private _installedPlugins: (Plugin | DisabledPlugin)[] = [];
private _pluginOrder: string[] = []; private _pluginOrder: string[] = [];
private _frozenPlugins: string[] = []; private _frozenPlugins: string[] = [];
private _hiddenPlugins: string[] = []; private _hiddenPlugins: string[] = [];
@@ -42,6 +46,8 @@ export class DeckyState {
publicState(): PublicDeckyState { publicState(): PublicDeckyState {
return { return {
plugins: this._plugins, plugins: this._plugins,
disabledPlugins: this._disabledPlugins,
installedPlugins: this._installedPlugins,
pluginOrder: this._pluginOrder, pluginOrder: this._pluginOrder,
frozenPlugins: this._frozenPlugins, frozenPlugins: this._frozenPlugins,
hiddenPlugins: this._hiddenPlugins, hiddenPlugins: this._hiddenPlugins,
@@ -62,6 +68,13 @@ export class DeckyState {
setPlugins(plugins: Plugin[]) { setPlugins(plugins: Plugin[]) {
this._plugins = plugins; this._plugins = plugins;
this._installedPlugins = [...plugins, ...this._disabledPlugins];
this.notifyUpdate();
}
setDisabledPlugins(disabledPlugins: DisabledPlugin[]) {
this._disabledPlugins = disabledPlugins;
this._installedPlugins = [...this._plugins, ...disabledPlugins];
this.notifyUpdate(); this.notifyUpdate();
} }
@@ -125,6 +138,7 @@ interface DeckyStateContext extends PublicDeckyState {
setIsLoaderUpdating(hasUpdate: boolean): void; setIsLoaderUpdating(hasUpdate: boolean): void;
setActivePlugin(name: string): void; setActivePlugin(name: string): void;
setPluginOrder(pluginOrder: string[]): void; setPluginOrder(pluginOrder: string[]): void;
setDisabledPlugins(disabled: DisabledPlugin[]): void;
closeActivePlugin(): void; closeActivePlugin(): void;
} }
@@ -163,6 +177,7 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
const setActivePlugin = deckyState.setActivePlugin.bind(deckyState); const setActivePlugin = deckyState.setActivePlugin.bind(deckyState);
const closeActivePlugin = deckyState.closeActivePlugin.bind(deckyState); const closeActivePlugin = deckyState.closeActivePlugin.bind(deckyState);
const setPluginOrder = deckyState.setPluginOrder.bind(deckyState); const setPluginOrder = deckyState.setPluginOrder.bind(deckyState);
const setDisabledPlugins = deckyState.setDisabledPlugins.bind(deckyState);
return ( return (
<DeckyStateContext.Provider <DeckyStateContext.Provider
@@ -173,6 +188,7 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
setActivePlugin, setActivePlugin,
closeActivePlugin, closeActivePlugin,
setPluginOrder, setPluginOrder,
setDisabledPlugins,
}} }}
> >
{children} {children}
+36 -10
View File
@@ -1,7 +1,7 @@
import { ButtonItem, ErrorBoundary, Focusable, PanelSection, PanelSectionRow } from '@decky/ui'; import { ButtonItem, ErrorBoundary, Focusable, PanelSection, PanelSectionRow } from '@decky/ui';
import { FC, useMemo } from 'react'; import { FC, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaEyeSlash } from 'react-icons/fa'; import { FaBan, FaEyeSlash } from 'react-icons/fa';
import { useDeckyState } from './DeckyState'; import { useDeckyState } from './DeckyState';
import NotificationBadge from './NotificationBadge'; import NotificationBadge from './NotificationBadge';
@@ -9,8 +9,16 @@ import { useQuickAccessVisible } from './QuickAccessVisibleState';
import TitleView from './TitleView'; import TitleView from './TitleView';
const PluginView: FC = () => { const PluginView: FC = () => {
const { plugins, hiddenPlugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = const {
useDeckyState(); plugins,
disabledPlugins,
hiddenPlugins,
updates,
activePlugin,
pluginOrder,
setActivePlugin,
closeActivePlugin,
} = useDeckyState();
const visible = useQuickAccessVisible(); const visible = useQuickAccessVisible();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -21,7 +29,9 @@ const PluginView: FC = () => {
.sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name)) .sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name))
.filter((p) => p.content) .filter((p) => p.content)
.filter(({ name }) => !hiddenPlugins.includes(name)); .filter(({ name }) => !hiddenPlugins.includes(name));
}, [plugins, pluginOrder]); }, [plugins, pluginOrder, hiddenPlugins]);
const numberOfHidden = hiddenPlugins.filter((name) => !!plugins.find((p) => p.name === name)).length;
if (activePlugin) { if (activePlugin) {
return ( return (
@@ -53,12 +63,28 @@ const PluginView: FC = () => {
</ButtonItem> </ButtonItem>
</PanelSectionRow> </PanelSectionRow>
))} ))}
{hiddenPlugins.length > 0 && ( <div
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem', marginTop: '10px' }}> style={{
<FaEyeSlash /> display: 'flex',
<div>{t('PluginView.hidden', { count: hiddenPlugins.length })}</div> flexDirection: 'column',
</div> 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>
</PanelSection> </PanelSection>
</div> </div>
</> </>
@@ -1,10 +1,10 @@
import { FC, ReactNode, createContext, useContext, useState } from 'react'; import { FC, PropsWithChildren, createContext, useContext, useState } from 'react';
const QuickAccessVisibleState = createContext<boolean>(false); const QuickAccessVisibleState = createContext<boolean>(false);
export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState); export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState);
export const QuickAccessVisibleStateProvider: FC<{ tab: any; children: ReactNode }> = ({ children, tab }) => { export const QuickAccessVisibleStateProvider: FC<PropsWithChildren<{ tab: any }>> = ({ children, tab }) => {
const initial = tab.initialVisibility; const initial = tab.initialVisibility;
const [visible, setVisible] = useState<boolean>(initial); const [visible, setVisible] = useState<boolean>(initial);
// HACK but i can't think of a better way to do this // HACK but i can't think of a better way to do this
-1
View File
@@ -8,7 +8,6 @@ import { useDeckyState } from './DeckyState';
const titleStyles: CSSProperties = { const titleStyles: CSSProperties = {
display: 'flex', display: 'flex',
paddingTop: '3px',
paddingRight: '16px', paddingRight: '16px',
position: 'sticky', position: 'sticky',
top: '0px', top: '0px',
+1 -1
View File
@@ -10,7 +10,7 @@ interface WithSuspenseProps {
const WithSuspense: FunctionComponent<WithSuspenseProps> = (props) => { const WithSuspense: FunctionComponent<WithSuspenseProps> = (props) => {
const propsCopy = { ...props }; const propsCopy = { ...props };
delete propsCopy.children; delete propsCopy.children;
(props.children as ReactElement)?.props && Object.assign((props.children as ReactElement).props, propsCopy); // There is probably a better way to do this but valve does it this way so ¯\_(ツ)_/¯ (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 ¯\_(ツ)_/¯
return ( return (
<Suspense <Suspense
fallback={ fallback={
@@ -3,10 +3,11 @@ import { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaCheck, FaDownload } from 'react-icons/fa'; import { FaCheck, FaDownload } from 'react-icons/fa';
import { InstallType, InstallTypeTranslationMapping } from '../../plugin'; import { DisabledPlugin, InstallType, InstallTypeTranslationMapping } from '../../plugin';
interface MultiplePluginsInstallModalProps { interface MultiplePluginsInstallModalProps {
requests: { name: string; version: string; hash: string; install_type: InstallType }[]; requests: { name: string; version: string; hash: string; install_type: InstallType }[];
disabledPlugins: DisabledPlugin[];
onOK(): void | Promise<void>; onOK(): void | Promise<void>;
onCancel(): void | Promise<void>; onCancel(): void | Promise<void>;
closeModal?(): void; closeModal?(): void;
@@ -17,6 +18,7 @@ type TitleTranslationMapping = 'mixed' | (typeof InstallTypeTranslationMapping)[
const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
requests, requests,
disabledPlugins,
onOK, onOK,
onCancel, onCancel,
closeModal, closeModal,
@@ -116,10 +118,11 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
version, version,
}); });
const disabled = disabledPlugins.some((p) => p.name === name);
return ( return (
<li key={i} style={{ display: 'flex', flexDirection: 'column' }}> <li key={i} style={{ display: 'flex', flexDirection: 'column' }}>
<span> <span>
{description}{' '} {disabled ? `${description} - ${t('PluginInstallModal.disabled')}` : description}{' '}
{(pluginsCompleted.includes(name) && <FaCheck />) || (name === pluginInProgress && <FaDownload />)} {(pluginsCompleted.includes(name) && <FaCheck />) || (name === pluginInProgress && <FaDownload />)}
</span> </span>
{hash === 'False' && ( {hash === 'False' && (
@@ -0,0 +1,39 @@
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,6 +9,7 @@ interface PluginInstallModalProps {
version: string; version: string;
hash: string; hash: string;
installType: InstallType; installType: InstallType;
disabled?: boolean;
onOK(): void; onOK(): void;
onCancel(): void; onCancel(): void;
closeModal?(): void; closeModal?(): void;
@@ -19,6 +20,7 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
version, version,
hash, hash,
installType, installType,
disabled,
onOK, onOK,
onCancel, onCancel,
closeModal, closeModal,
@@ -45,6 +47,10 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
}, []); }, []);
const installTypeTranslationKey = InstallTypeTranslationMapping[installType]; const installTypeTranslationKey = InstallTypeTranslationMapping[installType];
const description = t(`PluginInstallModal.${installTypeTranslationKey}.desc`, {
artifact: artifact,
version: version,
});
return ( return (
<ConfirmModal <ConfirmModal
@@ -118,10 +124,7 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
// t('PluginInstallModal.update.desc') // t('PluginInstallModal.update.desc')
// t('PluginInstallModal.downgrade.desc') // t('PluginInstallModal.downgrade.desc')
// t('PluginInstallModal.overwrite.desc') // t('PluginInstallModal.overwrite.desc')
t(`PluginInstallModal.${installTypeTranslationKey}.desc`, { disabled ? `${description} ${t('PluginInstallModal.disabled')}` : description
artifact: artifact,
version: version,
})
} }
</div> </div>
{hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>} {hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>}
@@ -2,8 +2,10 @@ import { ConfirmModal, Spinner } from '@decky/ui';
import { FC, useState } from 'react'; import { FC, useState } from 'react';
import { uninstallPlugin } from '../../plugin'; import { uninstallPlugin } from '../../plugin';
import { DeckyState } from '../DeckyState';
interface PluginUninstallModalProps { interface PluginUninstallModalProps {
deckyState: DeckyState;
name: string; name: string;
title: string; title: string;
buttonText: string; buttonText: string;
@@ -11,7 +13,14 @@ interface PluginUninstallModalProps {
closeModal?(): void; closeModal?(): void;
} }
const PluginUninstallModal: FC<PluginUninstallModalProps> = ({ name, title, buttonText, description, closeModal }) => { const PluginUninstallModal: FC<PluginUninstallModalProps> = ({
name,
title,
buttonText,
description,
deckyState,
closeModal,
}) => {
const [uninstalling, setUninstalling] = useState<boolean>(false); const [uninstalling, setUninstalling] = useState<boolean>(false);
return ( return (
<ConfirmModal <ConfirmModal
@@ -19,6 +28,7 @@ const PluginUninstallModal: FC<PluginUninstallModalProps> = ({ name, title, butt
onOK={async () => { onOK={async () => {
setUninstalling(true); setUninstalling(true);
await uninstallPlugin(name); await uninstallPlugin(name);
deckyState.setDisabledPlugins(deckyState.publicState().disabledPlugins.filter((d) => d.name !== name));
// uninstalling a plugin resets the hidden setting for it server-side // 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 // we invalidate here so if you re-install it, you won't have an out-of-date hidden filter
await DeckyPluginLoader.frozenPluginsService.invalidate(); await DeckyPluginLoader.frozenPluginsService.invalidate();
@@ -1,4 +1,4 @@
import { FC, useEffect, useState } from 'react'; import { FC, JSX, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { IconContext } from 'react-icons'; import { IconContext } from 'react-icons';
import { FaExclamationTriangle, FaQuestionCircle, FaUserSlash } from 'react-icons/fa'; import { FaExclamationTriangle, FaQuestionCircle, FaUserSlash } from 'react-icons/fa';
@@ -1,15 +1,16 @@
import { FC } from 'react'; import { FC } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaEyeSlash, FaLock } from 'react-icons/fa'; import { FaBan, FaEyeSlash, FaLock } from 'react-icons/fa';
interface PluginListLabelProps { interface PluginListLabelProps {
frozen: boolean; frozen: boolean;
hidden: boolean; hidden: boolean;
disabled: boolean;
name: string; name: string;
version?: string; version?: string;
} }
const PluginListLabel: FC<PluginListLabelProps> = ({ name, frozen, hidden, version }) => { const PluginListLabel: FC<PluginListLabelProps> = ({ name, frozen, hidden, version, disabled }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
@@ -43,6 +44,20 @@ const PluginListLabel: FC<PluginListLabelProps> = ({ name, frozen, hidden, versi
{t('PluginListLabel.hidden')} {t('PluginListLabel.hidden')}
</div> </div>
)} )}
{disabled && (
<div
style={{
fontSize: '0.8rem',
color: '#dcdedf',
display: 'flex',
alignItems: 'center',
gap: '10px',
}}
>
<FaBan />
{t('PluginListLabel.disabled')}
</div>
)}
</div> </div>
); );
}; };
@@ -2,9 +2,11 @@ import {
DialogBody, DialogBody,
DialogButton, DialogButton,
DialogControlsSection, DialogControlsSection,
Focusable,
GamepadEvent, GamepadEvent,
Menu, Menu,
MenuItem, MenuItem,
NavEntryPositionPreferences,
ReorderableEntry, ReorderableEntry,
ReorderableList, ReorderableList,
showContextMenu, showContextMenu,
@@ -13,7 +15,7 @@ import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaDownload, FaEllipsisH, FaRecycle } from 'react-icons/fa'; import { FaDownload, FaEllipsisH, FaRecycle } from 'react-icons/fa';
import { InstallType } from '../../../../plugin'; import { InstallType, enablePlugin } from '../../../../plugin';
import { import {
StorePluginVersion, StorePluginVersion,
getPluginList, getPluginList,
@@ -35,6 +37,7 @@ async function reinstallPlugin(pluginName: string, currentVersion?: string) {
type PluginTableData = PluginData & { type PluginTableData = PluginData & {
name: string; name: string;
disabled: boolean;
frozen: boolean; frozen: boolean;
onFreeze(): void; onFreeze(): void;
onUnfreeze(): void; onUnfreeze(): void;
@@ -54,22 +57,25 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
return null; return null;
} }
const { name, update, version, onHide, onShow, hidden, onFreeze, onUnfreeze, frozen, isDeveloper } = props.entry.data; const { name, update, version, onHide, onShow, hidden, onFreeze, onUnfreeze, frozen, isDeveloper, disabled } =
props.entry.data;
const showCtxMenu = (e: MouseEvent | GamepadEvent) => { const showCtxMenu = (e: MouseEvent | GamepadEvent) => {
showContextMenu( showContextMenu(
<Menu label={t('PluginListIndex.plugin_actions')}> <Menu label={t('PluginListIndex.plugin_actions')}>
<MenuItem {!disabled && (
onSelected={async () => { <MenuItem
try { onSelected={async () => {
await reloadPluginBackend(name); try {
} catch (err) { await reloadPluginBackend(name);
console.error('Error Reloading Plugin Backend', err); } catch (err) {
} console.error('Error Reloading Plugin Backend', err);
}} }
> }}
{t('PluginListIndex.reload')} >
</MenuItem> {t('PluginListIndex.reload')}
</MenuItem>
)}
<MenuItem <MenuItem
onSelected={() => onSelected={() =>
DeckyPluginLoader.uninstallPlugin( DeckyPluginLoader.uninstallPlugin(
@@ -82,11 +88,28 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
> >
{t('PluginListIndex.uninstall')} {t('PluginListIndex.uninstall')}
</MenuItem> </MenuItem>
{hidden ? ( {disabled ? (
<MenuItem onSelected={onShow}>{t('PluginListIndex.show')}</MenuItem> <MenuItem onSelected={() => enablePlugin(name)}>{t('PluginListIndex.enable')}</MenuItem>
) : ( ) : (
<MenuItem onSelected={onHide}>{t('PluginListIndex.hide')}</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>
)} )}
{!disabled &&
(hidden ? (
<MenuItem onSelected={onShow}>{t('PluginListIndex.show')}</MenuItem>
) : (
<MenuItem onSelected={onHide}>{t('PluginListIndex.hide')}</MenuItem>
))}
{frozen ? ( {frozen ? (
<MenuItem onSelected={onUnfreeze}>{t('PluginListIndex.unfreeze')}</MenuItem> <MenuItem onSelected={onUnfreeze}>{t('PluginListIndex.unfreeze')}</MenuItem>
) : ( ) : (
@@ -98,7 +121,7 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
}; };
return ( return (
<> <Focusable navEntryPreferPosition={NavEntryPositionPreferences.MAINTAIN_X} style={{ display: 'flex' }}>
{update ? ( {update ? (
<DialogButton <DialogButton
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }} style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
@@ -137,7 +160,7 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
> >
<FaEllipsisH /> <FaEllipsisH />
</DialogButton> </DialogButton>
</> </Focusable>
); );
} }
@@ -147,16 +170,18 @@ type PluginData = {
}; };
export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) { export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) {
const { plugins, updates, pluginOrder, setPluginOrder, frozenPlugins, hiddenPlugins } = useDeckyState(); const { installedPlugins, disabledPlugins, updates, pluginOrder, setPluginOrder, frozenPlugins, hiddenPlugins } =
useDeckyState();
const [_, setPluginOrderSetting] = useSetting<string[]>( const [_, setPluginOrderSetting] = useSetting<string[]>(
'pluginOrder', 'pluginOrder',
plugins.map((plugin) => plugin.name), installedPlugins.map((plugin) => plugin.name),
); );
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
DeckyPluginLoader.checkPluginUpdates(); DeckyPluginLoader.checkPluginUpdates();
}, []); }, [installedPlugins, frozenPlugins]);
const [pluginEntries, setPluginEntries] = useState<ReorderableEntry<PluginTableData>[]>([]); const [pluginEntries, setPluginEntries] = useState<ReorderableEntry<PluginTableData>[]>([]);
const hiddenPluginsService = DeckyPluginLoader.hiddenPluginsService; const hiddenPluginsService = DeckyPluginLoader.hiddenPluginsService;
@@ -164,15 +189,24 @@ export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) {
useEffect(() => { useEffect(() => {
setPluginEntries( setPluginEntries(
plugins.map(({ name, version }) => { installedPlugins.map(({ name, version }) => {
const frozen = frozenPlugins.includes(name); const frozen = frozenPlugins.includes(name);
const hidden = hiddenPlugins.includes(name); const hidden = hiddenPlugins.includes(name);
return { return {
label: <PluginListLabel name={name} frozen={frozen} hidden={hidden} version={version} />, label: (
<PluginListLabel
name={name}
frozen={frozen}
hidden={hidden}
version={version}
disabled={disabledPlugins.find((p) => p.name == name) !== undefined}
/>
),
position: pluginOrder.indexOf(name), position: pluginOrder.indexOf(name),
data: { data: {
name, name,
disabled: disabledPlugins.some((disabledPlugin) => disabledPlugin.name === name),
frozen, frozen,
hidden, hidden,
isDeveloper, isDeveloper,
@@ -186,9 +220,9 @@ export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) {
}; };
}), }),
); );
}, [plugins, updates, hiddenPlugins]); }, [installedPlugins, updates, hiddenPlugins, disabledPlugins]);
if (plugins.length === 0) { if (installedPlugins.length === 0) {
return ( return (
<div> <div>
<p>{t('PluginListIndex.no_plugin')}</p> <p>{t('PluginListIndex.no_plugin')}</p>
@@ -4,6 +4,7 @@ import {
DialogControlsSection, DialogControlsSection,
Field, Field,
Focusable, Focusable,
NavEntryPositionPreferences,
Navigation, Navigation,
ProgressBar, ProgressBar,
SteamSpinner, SteamSpinner,
@@ -87,7 +88,10 @@ export default function TestingVersionList() {
</> </>
} }
> >
<Focusable style={{ height: '40px', marginLeft: 'auto', display: 'flex' }}> <Focusable
style={{ height: '40px', marginLeft: 'auto', display: 'flex' }}
navEntryPreferPosition={NavEntryPositionPreferences.MAINTAIN_X}
>
<DialogButton <DialogButton
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }} style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
onClick={async () => { onClick={async () => {
+15 -4
View File
@@ -1,15 +1,23 @@
import { ButtonItem, Dropdown, Focusable, PanelSectionRow, SingleDropdownOption, SuspensefulImage } from '@decky/ui'; import {
ButtonItem,
Dropdown,
Focusable,
NavEntryPositionPreferences,
PanelSectionRow,
SingleDropdownOption,
SuspensefulImage,
} from '@decky/ui';
import { CSSProperties, FC, useState } from 'react'; import { CSSProperties, FC, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaArrowDown, FaArrowUp, FaCheck, FaDownload, FaRecycle } from 'react-icons/fa'; import { FaArrowDown, FaArrowUp, FaCheck, FaDownload, FaRecycle } from 'react-icons/fa';
import { InstallType, Plugin } from '../../plugin'; import { DisabledPlugin, InstallType, Plugin } from '../../plugin';
import { StorePlugin, requestPluginInstall } from '../../store'; import { StorePlugin, requestPluginInstall } from '../../store';
import ExternalLink from '../ExternalLink'; import ExternalLink from '../ExternalLink';
interface PluginCardProps { interface PluginCardProps {
storePlugin: StorePlugin; storePlugin: StorePlugin;
installedPlugin: Plugin | undefined; installedPlugin: Plugin | DisabledPlugin | undefined;
} }
const PluginCard: FC<PluginCardProps> = ({ storePlugin, installedPlugin }) => { const PluginCard: FC<PluginCardProps> = ({ storePlugin, installedPlugin }) => {
@@ -139,7 +147,10 @@ const PluginCard: FC<PluginCardProps> = ({ storePlugin, installedPlugin }) => {
</div> </div>
<div className="deckyStoreCardButtonRow"> <div className="deckyStoreCardButtonRow">
<PanelSectionRow> <PanelSectionRow>
<Focusable style={{ display: 'flex', gap: '5px', padding: 0 }}> <Focusable
style={{ display: 'flex', gap: '5px', padding: 0 }}
navEntryPreferPosition={NavEntryPositionPreferences.MAINTAIN_X}
>
<div <div
className="deckyStoreCardInstallContainer" className="deckyStoreCardInstallContainer"
style={ style={
+1 -1
View File
@@ -105,7 +105,7 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }>
})(); })();
}, []); }, []);
const { plugins: installedPlugins } = useDeckyState(); const { installedPlugins } = useDeckyState();
return ( return (
<> <>
+15 -5
View File
@@ -1,8 +1,4 @@
// Sets up DFL, then loads start.ts which starts up the loader // Sets up DFL, then loads start.ts which starts up the loader
interface Window {
// Shut up TS
SP_REACTDOM: any;
}
(async () => { (async () => {
console.debug('[Decky:Boot] Frontend init'); console.debug('[Decky:Boot] Frontend init');
@@ -21,7 +17,21 @@ interface Window {
// deliberate partial import // deliberate partial import
const DFLWebpack = await import('@decky/ui/dist/webpack'); const DFLWebpack = await import('@decky/ui/dist/webpack');
window.SP_REACT = DFLWebpack.findModule((m) => m.Component && m.PureComponent && m.useLayoutEffect); window.SP_REACT = DFLWebpack.findModule((m) => m.Component && m.PureComponent && m.useLayoutEffect);
window.SP_REACTDOM = DFLWebpack.findModule((m) => m.createPortal && m.createRoot); 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 jsxModule = DFLWebpack.findModule((m) => (m.jsx && m.jsxs) || (m.jsx && Object.keys(m).length == 1));
if (jsxModule.jsxs) {
window.SP_JSX = jsxModule;
} else {
window.SP_JSX = {
jsx: jsxModule.jsx,
jsxs: jsxModule.jsx,
Fragment: window.SP_REACT.Fragment,
};
}
} }
console.debug('[Decky:Boot] Setting up @decky/ui...'); console.debug('[Decky:Boot] Setting up @decky/ui...');
window.DFL = await import('@decky/ui'); window.DFL = await import('@decky/ui');
+89 -37
View File
@@ -19,6 +19,7 @@ import { DeckyState, DeckyStateContextProvider, UserInfo, useDeckyState } from '
import { File, FileSelectionType } from './components/modals/filepicker'; import { File, FileSelectionType } from './components/modals/filepicker';
import { deinitFilepickerPatches, initFilepickerPatches } from './components/modals/filepicker/patches'; import { deinitFilepickerPatches, initFilepickerPatches } from './components/modals/filepicker/patches';
import MultiplePluginsInstallModal from './components/modals/MultiplePluginsInstallModal'; import MultiplePluginsInstallModal from './components/modals/MultiplePluginsInstallModal';
import PluginDisableModal from './components/modals/PluginDisableModal';
import PluginInstallModal from './components/modals/PluginInstallModal'; import PluginInstallModal from './components/modals/PluginInstallModal';
import PluginUninstallModal from './components/modals/PluginUninstallModal'; import PluginUninstallModal from './components/modals/PluginUninstallModal';
import NotificationBadge from './components/NotificationBadge'; import NotificationBadge from './components/NotificationBadge';
@@ -30,7 +31,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, PluginLoadType } from './plugin'; import { DisabledPlugin, 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';
@@ -91,6 +92,7 @@ class PluginLoader extends Logger {
DeckyBackend.addEventListener('loader/notify_updates', this.notifyUpdates.bind(this)); DeckyBackend.addEventListener('loader/notify_updates', this.notifyUpdates.bind(this));
DeckyBackend.addEventListener('loader/import_plugin', this.importPlugin.bind(this)); DeckyBackend.addEventListener('loader/import_plugin', this.importPlugin.bind(this));
DeckyBackend.addEventListener('loader/unload_plugin', this.unloadPlugin.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_plugin_install_prompt', this.addPluginInstallPrompt.bind(this));
DeckyBackend.addEventListener( DeckyBackend.addEventListener(
'loader/add_multiple_plugins_install_prompt', 'loader/add_multiple_plugins_install_prompt',
@@ -120,28 +122,6 @@ class PluginLoader extends Logger {
<DeckyStateContextProvider deckyState={this.deckyState}> <DeckyStateContextProvider deckyState={this.deckyState}>
<FaPlug /> <FaPlug />
<TabBadge /> <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> </DeckyStateContextProvider>
), ),
}); });
@@ -197,7 +177,7 @@ class PluginLoader extends Logger {
private getPluginsFromBackend = DeckyBackend.callable< private getPluginsFromBackend = DeckyBackend.callable<
[], [],
{ name: string; version: string; load_type: PluginLoadType }[] { name: string; version: string; load_type: PluginLoadType; disabled: boolean }[]
>('loader/get_plugins'); >('loader/get_plugins');
private restartWebhelper = DeckyBackend.callable<[], void>('utilities/restart_webhelper'); private restartWebhelper = DeckyBackend.callable<[], void>('utilities/restart_webhelper');
@@ -220,10 +200,16 @@ class PluginLoader extends Logger {
this.runCrashChecker(); this.runCrashChecker();
const plugins = await this.getPluginsFromBackend(); const plugins = await this.getPluginsFromBackend();
const pluginLoadPromises = []; const pluginLoadPromises = [];
const disabledPlugins: DisabledPlugin[] = [];
const loadStart = performance.now(); const loadStart = performance.now();
for (const plugin of plugins) { for (const plugin of plugins) {
if (!this.hasPlugin(plugin.name)) if (plugin.disabled) {
pluginLoadPromises.push(this.importPlugin(plugin.name, plugin.version, plugin.load_type, false)); 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));
}
} }
await Promise.all(pluginLoadPromises); await Promise.all(pluginLoadPromises);
const loadEnd = performance.now(); const loadEnd = performance.now();
@@ -274,7 +260,9 @@ class PluginLoader extends Logger {
public async checkPluginUpdates() { public async checkPluginUpdates() {
const frozenPlugins = this.deckyState.publicState().frozenPlugins; const frozenPlugins = this.deckyState.publicState().frozenPlugins;
const updates = await checkForPluginUpdates(this.plugins.filter((p) => !frozenPlugins.includes(p.name))); const updates = await checkForPluginUpdates(
this.deckyState.publicState().installedPlugins.filter((p) => !frozenPlugins.includes(p.name)),
);
this.deckyState.setUpdates(updates); this.deckyState.setUpdates(updates);
return updates; return updates;
} }
@@ -312,6 +300,7 @@ class PluginLoader extends Logger {
version={version} version={version}
hash={hash} hash={hash}
installType={install_type} installType={install_type}
disabled={this.deckyState.publicState().disabledPlugins.some((p) => p.name === artifact)}
onOK={() => DeckyBackend.call<[string]>('utilities/confirm_plugin_install', request_id)} onOK={() => DeckyBackend.call<[string]>('utilities/confirm_plugin_install', request_id)}
onCancel={() => DeckyBackend.call<[string]>('utilities/cancel_plugin_install', request_id)} onCancel={() => DeckyBackend.call<[string]>('utilities/cancel_plugin_install', request_id)}
/>, />,
@@ -325,6 +314,7 @@ class PluginLoader extends Logger {
showModal( showModal(
<MultiplePluginsInstallModal <MultiplePluginsInstallModal
requests={requests} requests={requests}
disabledPlugins={this.deckyState.publicState().disabledPlugins}
onOK={() => DeckyBackend.call<[string]>('utilities/confirm_plugin_install', request_id)} onOK={() => DeckyBackend.call<[string]>('utilities/confirm_plugin_install', request_id)}
onCancel={() => DeckyBackend.call<[string]>('utilities/cancel_plugin_install', request_id)} onCancel={() => DeckyBackend.call<[string]>('utilities/cancel_plugin_install', request_id)}
/>, />,
@@ -332,7 +322,19 @@ class PluginLoader extends Logger {
} }
public uninstallPlugin(name: string, title: string, buttonText: string, description: string) { public uninstallPlugin(name: string, title: string, buttonText: string, description: string) {
showModal(<PluginUninstallModal name={name} title={title} buttonText={buttonText} description={description} />); 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} />);
} }
public hasPlugin(name: string) { public hasPlugin(name: string) {
@@ -373,6 +375,19 @@ class PluginLoader extends Logger {
this.errorBoundaryHook.deinit(); 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) { public unloadPlugin(name: string, skipStateUpdate: boolean = false) {
const plugin = this.plugins.find((plugin) => plugin.name === name); const plugin = this.plugins.find((plugin) => plugin.name === name);
plugin?.onDismount?.(); plugin?.onDismount?.();
@@ -385,6 +400,7 @@ class PluginLoader extends Logger {
version?: string | undefined, version?: string | undefined,
loadType: PluginLoadType = PluginLoadType.ESMODULE_V1, loadType: PluginLoadType = PluginLoadType.ESMODULE_V1,
useQueue: boolean = true, useQueue: boolean = true,
timeoutMS?: number,
) { ) {
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);
@@ -398,9 +414,11 @@ class PluginLoader extends Logger {
this.unloadPlugin(name, true); this.unloadPlugin(name, true);
const startTime = performance.now(); const startTime = performance.now();
await this.importReactPlugin(name, version, loadType);
await this.importReactPlugin(name, version, loadType, timeoutMS);
const endTime = performance.now(); const endTime = performance.now();
this.deckyState.setDisabledPlugins(this.deckyState.publicState().disabledPlugins.filter((d) => d.name !== name));
this.deckyState.setPlugins(this.plugins); this.deckyState.setPlugins(this.plugins);
this.log(`Loaded ${name} in ${endTime - startTime}ms`); this.log(`Loaded ${name} in ${endTime - startTime}ms`);
} catch (e) { } catch (e) {
@@ -410,7 +428,7 @@ class PluginLoader extends Logger {
this.reloadLock = false; this.reloadLock = false;
const nextPlugin = this.pluginReloadQueue.shift(); const nextPlugin = this.pluginReloadQueue.shift();
if (nextPlugin) { if (nextPlugin) {
this.importPlugin(nextPlugin.name, nextPlugin.version, loadType); this.importPlugin(nextPlugin.name, nextPlugin.version, nextPlugin.loadType, true, timeoutMS);
} }
} }
} }
@@ -420,12 +438,28 @@ class PluginLoader extends Logger {
name: string, name: string,
version?: string, version?: string,
loadType: PluginLoadType = PluginLoadType.ESMODULE_V1, loadType: PluginLoadType = PluginLoadType.ESMODULE_V1,
timeoutMS?: number,
) { ) {
let spExists = this.checkForSP(); let spExists = this.checkForSP();
const timeoutException = new Error(
`${name} failed to load within ${timeoutMS ? `${timeoutMS / 1000} second` : ''} time limit`,
);
let timeout: number | undefined;
try { try {
switch (loadType) { switch (loadType) {
case PluginLoadType.ESMODULE_V1: case PluginLoadType.ESMODULE_V1:
const plugin_exports = await import(`http://127.0.0.1:1337/plugins/${name}/dist/index.js?t=${Date.now()}`); 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;
let plugin = plugin_exports.default(); let plugin = plugin_exports.default();
this.plugins.push({ this.plugins.push({
@@ -437,12 +471,26 @@ class PluginLoader extends Logger {
break; break;
case PluginLoadType.LEGACY_EVAL_IIFE: case PluginLoadType.LEGACY_EVAL_IIFE:
let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, { const fetchJS = async () => {
credentials: 'include', const controller = new AbortController();
headers: { const { signal } = controller;
'X-Decky-Auth': deckyAuthToken,
}, 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();
if (res.ok) { if (res.ok) {
let plugin_export: (serverAPI: any) => Plugin = await eval( let plugin_export: (serverAPI: any) => Plugin = await eval(
(await res.text()) + `\n//# sourceURL=decky://decky/legacy_plugin/${encodeURIComponent(name)}/index.js`, (await res.text()) + `\n//# sourceURL=decky://decky/legacy_plugin/${encodeURIComponent(name)}/index.js`,
@@ -461,6 +509,8 @@ class PluginLoader extends Logger {
throw new Error(`${name} has no defined loadType.`); throw new Error(`${name} has no defined loadType.`);
} }
} catch (e) { } catch (e) {
if (e === timeoutException) throw timeoutException;
this.error('Error loading plugin ' + name, e); this.error('Error loading plugin ' + name, e);
const TheError: FC<{}> = () => ( const TheError: FC<{}> = () => (
<PanelSection> <PanelSection>
@@ -503,6 +553,8 @@ class PluginLoader extends Logger {
body: '' + e, body: '' + e,
icon: <FaExclamationCircle />, icon: <FaExclamationCircle />,
}); });
} finally {
if (timeout !== undefined) clearTimeout(timeout);
} }
if (spExists && !this.checkForSP()) { if (spExists && !this.checkForSP()) {
+5
View File
@@ -1,3 +1,4 @@
import type { JSX } from 'react';
export enum PluginLoadType { export enum PluginLoadType {
LEGACY_EVAL_IIFE = 0, // legacy, uses legacy serverAPI LEGACY_EVAL_IIFE = 0, // legacy, uses legacy serverAPI
ESMODULE_V1 = 1, // esmodule loading with modern @decky/backend apis ESMODULE_V1 = 1, // esmodule loading with modern @decky/backend apis
@@ -14,6 +15,8 @@ export interface Plugin {
titleView?: JSX.Element; titleView?: JSX.Element;
} }
export type DisabledPlugin = Pick<Plugin, 'name' | 'version'>;
export enum InstallType { export enum InstallType {
INSTALL, INSTALL,
REINSTALL, REINSTALL,
@@ -55,3 +58,5 @@ type installPluginsArgs = [
export let installPlugins = DeckyBackend.callable<installPluginsArgs>('utilities/install_plugins'); export let installPlugins = DeckyBackend.callable<installPluginsArgs>('utilities/install_plugins');
export let uninstallPlugin = DeckyBackend.callable<[name: string]>('utilities/uninstall_plugin'); 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, getReactRoot,
sleep, sleep,
} from '@decky/ui'; } from '@decky/ui';
import { FC, ReactElement, ReactNode, cloneElement, createElement } from 'react'; import { FC, JSX, ReactElement, ReactNode, cloneElement, createElement } from 'react';
import type { Route } from 'react-router'; import type { Route } from 'react-router';
import { import {
@@ -37,7 +37,7 @@ const isPatched = Symbol('is patched');
class RouterHook extends Logger { class RouterHook extends Logger {
private routerState: DeckyRouterState = new DeckyRouterState(); private routerState: DeckyRouterState = new DeckyRouterState();
private globalComponentsState: DeckyGlobalComponentsState = new DeckyGlobalComponentsState(); private globalComponentsState: DeckyGlobalComponentsState = new DeckyGlobalComponentsState();
private renderedComponents: ReactElement[] = []; private renderedComponents: ReactElement<any>[] = [];
private Route: any; private Route: any;
private DeckyGamepadRouterWrapper = this.gamepadRouterWrapper.bind(this); private DeckyGamepadRouterWrapper = this.gamepadRouterWrapper.bind(this);
private DeckyDesktopRouterWrapper = this.desktopRouterWrapper.bind(this); private DeckyDesktopRouterWrapper = this.desktopRouterWrapper.bind(this);
@@ -233,7 +233,7 @@ class RouterHook extends Logger {
return <>{this.renderedComponents}</>; return <>{this.renderedComponents}</>;
} }
private gamepadRouterWrapper({ children }: { children: ReactElement }) { private gamepadRouterWrapper({ children }: { children: ReactElement<any> }) {
// Used to store the new replicated routes we create to allow routes to be unpatched. // Used to store the new replicated routes we create to allow routes to be unpatched.
const { routes, routePatches } = useDeckyRouterState(); const { routes, routePatches } = useDeckyRouterState();
@@ -251,7 +251,7 @@ class RouterHook extends Logger {
return children; return children;
} }
private desktopRouterWrapper({ children }: { children: ReactElement }) { private desktopRouterWrapper({ children }: { children: ReactElement<any> }) {
// Used to store the new replicated routes we create to allow routes to be unpatched. // Used to store the new replicated routes we create to allow routes to be unpatched.
this.debug('desktop router wrapper render', children); this.debug('desktop router wrapper render', children);
const { routes, routePatches } = useDeckyRouterState(); const { routes, routePatches } = useDeckyRouterState();
@@ -287,7 +287,7 @@ class RouterHook extends Logger {
if (routes) { if (routes) {
if (!routeList[routerIndex - 1]?.length || routeList[routerIndex - 1]?.length !== routes.size) { if (!routeList[routerIndex - 1]?.length || routeList[routerIndex - 1]?.length !== routes.size) {
if (routeList[routerIndex - 1]?.length && routeList[routerIndex - 1].length !== routes.size) routerIndex--; if (routeList[routerIndex - 1]?.length && routeList[routerIndex - 1].length !== routes.size) routerIndex--;
const newRouterArray: (ReactElement | JSX.Element)[] = []; const newRouterArray: (ReactElement<any> | JSX.Element)[] = [];
routes.forEach(({ component, props }, path) => { routes.forEach(({ component, props }, path) => {
newRouterArray.push( newRouterArray.push(
<Route path={path} {...props}> <Route path={path} {...props}>
+7 -4
View File
@@ -1,6 +1,6 @@
import { compare } from 'compare-versions'; import { compare, validate } from 'compare-versions';
import { InstallType, Plugin, installPlugin, installPlugins } from './plugin'; import { DisabledPlugin, InstallType, Plugin, installPlugin, installPlugins } from './plugin';
import { getSetting, setSetting } from './utils/settings'; import { getSetting, setSetting } from './utils/settings';
export enum Store { export enum Store {
@@ -113,18 +113,21 @@ export async function requestMultiplePluginInstalls(requests: PluginInstallReque
); );
} }
export async function checkForPluginUpdates(plugins: Plugin[]): Promise<PluginUpdateMapping> { export async function checkForPluginUpdates(plugins: (Plugin | DisabledPlugin)[]): Promise<PluginUpdateMapping> {
const serverData = await getPluginList(); const serverData = await getPluginList();
const updateMap = new Map<string, StorePluginVersion>(); const updateMap = new Map<string, StorePluginVersion>();
for (let plugin of plugins) { for (let plugin of plugins) {
const remotePlugin = serverData?.find((x) => x.name == plugin.name); const remotePlugin = serverData?.find((x) => x.name == plugin.name);
//FIXME: Ugly hack since plugin.version might be null during evaluation, //FIXME: Ugly hack since plugin.version might be null during evaluation,
//so this will set the older version possible //so this will set the older version possible
const curVer = plugin.version ? plugin.version : '0.0'; const curVer = plugin.version ? plugin.version : '0.0.0';
if ( if (
remotePlugin && remotePlugin &&
remotePlugin.versions?.length > 0 && remotePlugin.versions?.length > 0 &&
plugin.version != remotePlugin?.versions?.[0]?.name && plugin.version != remotePlugin?.versions?.[0]?.name &&
validate(remotePlugin.versions?.[0]?.name) &&
validate(curVer) &&
compare(remotePlugin?.versions?.[0]?.name, curVer, '>') compare(remotePlugin?.versions?.[0]?.name, curVer, '>')
) { ) {
updateMap.set(plugin.name, remotePlugin.versions[0]); updateMap.set(plugin.name, remotePlugin.versions[0]);
+19 -6
View File
@@ -29,7 +29,8 @@ interface Tab {
class TabsHook extends Logger { class TabsHook extends Logger {
// private keys = 7; // private keys = 7;
tabs: Tab[] = []; tabs: Tab[] = [];
private qamPatch?: Patch; private qamBrowserViewPatch?: Patch;
private qamEmbeddedPatch?: Patch;
constructor() { constructor() {
super('TabsHook'); super('TabsHook');
@@ -40,11 +41,13 @@ class TabsHook extends Logger {
} }
init() { init() {
// TODO patch the "embedded" renderer in this module too (seems to be for VR? unsure)
const qamModule = findModuleByExport((e) => e?.type?.toString?.()?.includes('QuickAccessMenuBrowserView')); const qamModule = findModuleByExport((e) => e?.type?.toString?.()?.includes('QuickAccessMenuBrowserView'));
const qamRenderer = Object.values(qamModule).find((e: any) => const qamBrowserViewRenderer = Object.values(qamModule).find((e: any) =>
e?.type?.toString?.()?.includes('QuickAccessMenuBrowserView'), e?.type?.toString?.()?.includes('QuickAccessMenuBrowserView'),
); );
const qamEmbeddedRenderer = Object.values(qamModule).find((e: any) =>
e?.type?.toString?.()?.includes('QuickAccessMenuEmbedded'),
);
const patchHandler = createReactTreePatcher( const patchHandler = createReactTreePatcher(
[(tree) => findInReactTree(tree, (node) => node?.props?.onFocusNavDeactivated)], [(tree) => findInReactTree(tree, (node) => node?.props?.onFocusNavDeactivated)],
@@ -56,12 +59,21 @@ class TabsHook extends Logger {
'TabsHook', 'TabsHook',
); );
this.qamPatch = afterPatch(qamRenderer, 'type', patchHandler); this.qamBrowserViewPatch = afterPatch(qamBrowserViewRenderer, 'type', patchHandler);
if (qamEmbeddedRenderer) this.qamEmbeddedPatch = afterPatch(qamEmbeddedRenderer, 'type', patchHandler);
// Patch already rendered qam // Patch already rendered qam
const root = getReactRoot(document.getElementById('root') as any); const root = getReactRoot(document.getElementById('root') as any);
const qamNode = root && findInReactTree(root, (n: any) => n.elementType == qamRenderer); // need elementType, because type is actually mobx wrapper const qamNode =
root &&
findInReactTree(
root,
(n: any) =>
n.elementType == qamBrowserViewRenderer ||
(qamEmbeddedRenderer != null && n.elementType == qamEmbeddedRenderer),
); // need elementType, because type is actually mobx wrapper
if (qamNode) { if (qamNode) {
console.log('patching existing qam');
// Only affects this fiber node so we don't need to unpatch here // Only affects this fiber node so we don't need to unpatch here
qamNode.type = qamNode.elementType.type; qamNode.type = qamNode.elementType.type;
if (qamNode?.alternate) { if (qamNode?.alternate) {
@@ -71,7 +83,8 @@ class TabsHook extends Logger {
} }
deinit() { deinit() {
this.qamPatch?.unpatch(); this.qamBrowserViewPatch?.unpatch();
this.qamEmbeddedPatch?.unpatch();
} }
add(tab: Tab) { add(tab: Tab) {
+2 -2
View File
@@ -1,6 +1,6 @@
import { ErrorInfo } from 'react'; import { ErrorInfo } from 'react';
const pluginErrorRegex = /http:\/\/localhost:1337\/plugins\/([^\/]*)\//; const pluginErrorRegex = /http:\/\/(?:localhost|127\.0\.0\.1):1337\/plugins\/([^\/]*)\//;
const pluginSourceMapErrorRegex = /decky:\/\/decky\/plugin\/([^\/]*)\//; const pluginSourceMapErrorRegex = /decky:\/\/decky\/plugin\/([^\/]*)\//;
const legacyPluginErrorRegex = /decky:\/\/decky\/legacy_plugin\/([^\/]*)\/index.js/; const legacyPluginErrorRegex = /decky:\/\/decky\/legacy_plugin\/([^\/]*)\/index.js/;
@@ -44,7 +44,7 @@ export function getLikelyErrorSource(error?: string): ErrorSource {
return [decodeURIComponent(legacyPluginMatch[1]), true, false]; return [decodeURIComponent(legacyPluginMatch[1]), true, false];
} }
if (error?.includes('http://localhost:1337/')) { if (error?.includes('http://localhost:1337/') || error?.includes('http://127.0.0.1:1337/')) {
return ['the Decky frontend', false, false]; return ['the Decky frontend', false, false];
} }
return ['Steam', false, true]; return ['Steam', false, true];
+3 -4
View File
@@ -2,9 +2,7 @@
"compilerOptions": { "compilerOptions": {
"module": "ESNext", "module": "ESNext",
"target": "ES2021", "target": "ES2021",
"jsx": "react", "jsx": "react-jsx",
"jsxFactory": "window.SP_REACT.createElement",
"jsxFragmentFactory": "window.SP_REACT.Fragment",
"declaration": false, "declaration": false,
"moduleResolution": "node", "moduleResolution": "node",
"noUnusedLocals": true, "noUnusedLocals": true,
@@ -15,7 +13,8 @@
"noImplicitAny": true, "noImplicitAny": true,
"strict": true, "strict": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"resolveJsonModule": true "resolveJsonModule": true,
"skipLibCheck": true
}, },
"include": ["src", "index.d.ts"], "include": ["src", "index.d.ts"],
"exclude": ["node_modules"] "exclude": ["node_modules"]