diff options
30 files changed, 472 insertions, 281 deletions
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b8a0f724..ac1637e4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,7 +36,7 @@ jobs: uses: actions/checkout@v4 - name: Install semver-tool asdf - uses: asdf-vm/actions/install@v3 + uses: asdf-vm/actions/install@v4 with: tool_versions: | semver 3.4.0 @@ -3,7 +3,7 @@ <br> Decky Loader <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> <p align="center"> @@ -18,6 +18,15 @@ <!-- <img src="https://media.discordapp.net/attachments/966017112244125756/1012466063893610506/main.jpg" alt="Decky screenshot" width="80%">--> </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 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). ## 💾 Installation + - This installation can be done without an admin/sudo password set. + 1. Prepare a mouse and keyboard if possible. - 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) @@ -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. Choose the version of Decky Loader you want to install. - **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** 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. @@ -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. Select "Switch to Desktop". 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. ## 🚀 Getting Started diff --git a/backend/decky_loader/browser.py b/backend/decky_loader/browser.py index 975a917a..fe8ae71a 100644 --- a/backend/decky_loader/browser.py +++ b/backend/decky_loader/browser.py @@ -150,6 +150,7 @@ class PluginBrowser: # plugins_snapshot = self.plugins.copy() # snapshot_string = pformat(plugins_snapshot) # logger.debug("current plugins: %s", snapshot_string) + if name in self.plugins: logger.debug("Plugin %s was found", name) await self.plugins[name].stop(uninstall=True) @@ -345,5 +346,10 @@ class PluginBrowser: if name in plugin_order: plugin_order.remove(name) self.settings.setSetting("pluginOrder", plugin_order) + + disabled_plugins: List[str] = self.settings.getSetting("disabled_plugins", []) + if name in disabled_plugins: + disabled_plugins.remove(name) + self.settings.setSetting("disabled_plugins", disabled_plugins) logger.debug("Removed any settings for plugin %s", name) diff --git a/backend/decky_loader/loader.py b/backend/decky_loader/loader.py index e2e619f7..4574cd1d 100644 --- a/backend/decky_loader/loader.py +++ b/backend/decky_loader/loader.py @@ -78,6 +78,7 @@ class Loader: self.live_reload = live_reload self.reload_queue: ReloadQueue = Queue() self.loop.create_task(self.handle_reloads()) + self.context: PluginManager = server_instance if live_reload: self.observer = Observer() @@ -130,7 +131,7 @@ class Loader: async def get_plugins(self): plugins = list(self.plugins.values()) - return [{"name": str(i), "version": i.version, "load_type": i.load_type} 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): 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}) plugin = PluginWrapper(file, plugin_directory, self.plugin_path, plugin_emitted_event) + if hasattr(self.context, "utilities") and plugin.name in await self.context.utilities.get_setting("disabled_plugins",[]): + plugin.disabled = True + self.plugins[plugin.name] = plugin + return if plugin.name in self.plugins: if not "debug" in plugin.flags and refresh: self.logger.info(f"Plugin {plugin.name} is already loaded and has requested to not be re-loaded") @@ -183,7 +188,7 @@ class Loader: print_exc() async def dispatch_plugin(self, name: str, version: str | None, load_type: int = PluginLoadType.ESMODULE_V1.value): - await self.ws.emit("loader/import_plugin", name, version, load_type) + await self.ws.emit("loader/import_plugin", name, version, load_type, True, 15000) async def import_plugins(self): self.logger.info(f"import plugins from {self.plugin_path}") diff --git a/backend/decky_loader/locales/en-US.json b/backend/decky_loader/locales/en-US.json index 836f4878..1f87fe3b 100644 --- a/backend/decky_loader/locales/en-US.json +++ b/backend/decky_loader/locales/en-US.json @@ -102,6 +102,7 @@ }, "no_hash": "This plugin does not have a hash, you are installing it at your own risk.", "not_installed": "(not installed)", + "disabled": "The plugin will be re-enabled after installation", "overwrite": { "button_idle": "Overwrite", "button_processing": "Overwriting", @@ -133,10 +134,13 @@ "uninstall": "Uninstall", "update_all_one": "Update 1 plugin", "update_all_other": "Update {{count}} plugins", - "update_to": "Update to {{name}}" + "update_to": "Update to {{name}}", + "disable": "Disable", + "enable": "Enable" }, "PluginListLabel": { - "hidden": "Hidden from the quick access menu" + "hidden": "Hidden from the quick access menu", + "disabled": "Plugin disabled" }, "PluginLoader": { "decky_title": "Decky", @@ -152,12 +156,23 @@ "desc": "Are you sure you want to uninstall {{name}}?", "title": "Uninstall {{name}}" }, + "plugin_disable": { + "button": "Disable", + "desc": "Are you sure you want to disable {{name}}?", + "title": "Disable {{name}}", + "error": "Error disabling {{name}}" + }, + "plugin_enable": { + "error": "Error enabling {{name}}" + }, "plugin_update_one": "Updates available for 1 plugin!", "plugin_update_other": "Updates available for {{count}} plugins!" }, "PluginView": { "hidden_one": "1 plugin is hidden from this list", - "hidden_other": "{{count}} plugins are hidden from this list" + "hidden_other": "{{count}} plugins are hidden from this list", + "disabled_one": "1 plugin is disabled", + "disabled_other": "{{count}} plugins are disabled" }, "RemoteDebugging": { "remote_cef": { diff --git a/backend/decky_loader/localplatform/localplatformlinux.py b/backend/decky_loader/localplatform/localplatformlinux.py index 1c8f2ace..21993e4c 100644 --- a/backend/decky_loader/localplatform/localplatformlinux.py +++ b/backend/decky_loader/localplatform/localplatformlinux.py @@ -116,28 +116,26 @@ def get_username() -> str: return _get_user() def setgid(user : UserType = UserType.HOST_USER): - user_id = 0 - - if user == UserType.HOST_USER: - user_id = _get_user_group_id() + host_user_group_id, effective_user_group_id = _get_user_group_id(), _get_effective_user_group_id() + if host_user_group_id == effective_user_group_id: + pass + elif user == UserType.HOST_USER: + os.setgid(host_user_group_id) elif user == UserType.EFFECTIVE_USER: - pass # we already are + os.setgid(effective_user_group_id) else: raise Exception("Unknown user type") - - os.setgid(user_id) def setuid(user : UserType = UserType.HOST_USER): - user_id = 0 - - if user == UserType.HOST_USER: - user_id = _get_user_id() + host_user_id, effective_user_id = _get_user_id(), _get_effective_user_id() + if host_user_id == effective_user_id: + pass + elif user == UserType.HOST_USER: + os.setuid(host_user_id) elif user == UserType.EFFECTIVE_USER: - pass # we already are + os.setuid(effective_user_id) else: raise Exception("Unknown user type") - - os.setuid(user_id) async def service_active(service_name : str) -> bool: res, _, _ = await run(["systemctl", "is-active", service_name], stdout=DEVNULL, stderr=DEVNULL) diff --git a/backend/decky_loader/plugin/plugin.py b/backend/decky_loader/plugin/plugin.py index 61de4b1f..a7edaa45 100644 --- a/backend/decky_loader/plugin/plugin.py +++ b/backend/decky_loader/plugin/plugin.py @@ -41,6 +41,7 @@ class PluginWrapper: self.author = json["author"] self.flags = json["flags"] self.api_version = json["api_version"] if "api_version" in json else 0 + self.disabled = False self.passive = not path.isfile(self.file) diff --git a/backend/decky_loader/plugin/sandboxed_plugin.py b/backend/decky_loader/plugin/sandboxed_plugin.py index 71e1d17b..20da747e 100644 --- a/backend/decky_loader/plugin/sandboxed_plugin.py +++ b/backend/decky_loader/plugin/sandboxed_plugin.py @@ -14,6 +14,7 @@ from ..localplatform.localsocket import LocalSocket from ..localplatform.localplatform import setgid, setuid, get_username, get_home_path, ON_LINUX from ..enums import UserType from .. import helpers +from .. import settings # pyright: ignore [reportUnusedImport] from typing import List, TypeVar, Any diff --git a/backend/decky_loader/utilities.py b/backend/decky_loader/utilities.py index 69c69fe6..75593fd5 100644 --- a/backend/decky_loader/utilities.py +++ b/backend/decky_loader/utilities.py @@ -1,5 +1,5 @@ from __future__ import annotations -from os import stat_result +from os import path, stat_result import uuid from urllib.parse import unquote from json.decoder import JSONDecodeError @@ -8,7 +8,7 @@ import re from traceback import format_exc from stat import FILE_ATTRIBUTE_HIDDEN # pyright: ignore [reportAttributeAccessIssue, reportUnknownVariableType] -from asyncio import StreamReader, StreamWriter, start_server, gather, open_connection +from asyncio import StreamReader, StreamWriter, sleep, start_server, gather, open_connection from aiohttp import ClientSession, hdrs from aiohttp.web import Request, StreamResponse, Response, json_response, post from typing import TYPE_CHECKING, Callable, Coroutine, Dict, Any, List, TypedDict @@ -80,6 +80,8 @@ class Utilities: context.ws.add_route("utilities/restart_webhelper", self.restart_webhelper) context.ws.add_route("utilities/close_cef_socket", self.close_cef_socket) context.ws.add_route("utilities/_call_legacy_utility", self._call_legacy_utility) + context.ws.add_route("utilities/enable_plugin", self.enable_plugin) + context.ws.add_route("utilities/disable_plugin", self.disable_plugin) context.web_app.add_routes([ post("/methods/{method_name}", self._handle_legacy_server_method_call) @@ -214,7 +216,7 @@ class Utilities: async def http_request_legacy(self, method: str, url: str, extra_opts: Any = {}, timeout: int | None = None): async with ClientSession() as web: - res = await web.request(method, url, ssl=helpers.get_ssl_context(), timeout=timeout, **extra_opts) + res = await web.request(method, url, ssl=helpers.get_ssl_context(), timeout=timeout, **extra_opts) # type: ignore text = await res.text() return { "status": res.status, @@ -390,7 +392,6 @@ class Utilities: "total": len(all), } - # Based on https://stackoverflow.com/a/46422554/13174603 def start_rdt_proxy(self, ip: str, port: int): async def pipe(reader: StreamReader, writer: StreamWriter): @@ -474,3 +475,32 @@ class Utilities: async def get_tab_id(self, name: str): return (await get_tab(name)).id + + async def disable_plugin(self, name: str): + disabled_plugins: List[str] = await self.get_setting("disabled_plugins", []) + if name not in disabled_plugins: + disabled_plugins.append(name) + await self.set_setting("disabled_plugins", disabled_plugins) + + await self.context.plugin_loader.plugins[name].stop() + await self.context.ws.emit("loader/disable_plugin", name) + + async def enable_plugin(self, name: str): + plugin_folder = self.context.plugin_browser.find_plugin_folder(name) + assert plugin_folder is not None + plugin_dir = path.join(self.context.plugin_browser.plugin_path, plugin_folder) + + if name in self.context.plugin_loader.plugins: + plugin = self.context.plugin_loader.plugins[name] + if plugin.proc and plugin.proc.is_alive(): + await plugin.stop() + self.context.plugin_loader.plugins.pop(name, None) + await sleep(1) + + disabled_plugins: List[str] = await self.get_setting("disabled_plugins", []) + + if name in disabled_plugins: + disabled_plugins.remove(name) + await self.set_setting("disabled_plugins", disabled_plugins) + + await self.context.plugin_loader.import_plugin(path.join(plugin_dir, "main.py"), plugin_folder)
\ No newline at end of file diff --git a/docs/images/download_button.svg b/docs/images/download_button.svg index 9f5d28b1..3a95878b 100644 --- a/docs/images/download_button.svg +++ b/docs/images/download_button.svg @@ -1,162 +1,27 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Created with Inkscape (http://www.inkscape.org/) --> +<?xml version="1.0" encoding="UTF-8"?> +<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 - width="81.700577mm" - height="24.334814mm" - viewBox="0 0 81.700577 24.334814" - 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" /> + .st1 { + fill: #fff; + } + </style> </defs> - <g - inkscape:label="Layer 1" - inkscape:groupmode="layer" - id="layer1" - transform="translate(-64.149712,-136.3326)"> - <rect - style="mix-blend-mode:normal;fill:url(#linearGradient13802);fill-opacity:1;stroke:none;stroke-width:0.271121" - id="rect111" - 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="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> + <rect class="st0" x="0" y="0" width="176.36" height="38" rx="19" ry="19"/> + <g> + <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"/> + <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"/> + <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"/> + <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"/> + <path class="st1" d="M118.22,26.66V9.98h2.56v16.67h-2.56Z"/> + <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"/> + <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"/> + <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"/> </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>
\ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 12d9ce85..b0eb2f51 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,7 @@ "localize": "i18next" }, "devDependencies": { - "@decky/api": "^1.1.1", + "@decky/api": "^1.1.3", "@rollup/plugin-commonjs": "^26.0.1", "@rollup/plugin-image": "^3.0.3", "@rollup/plugin-json": "^6.1.0", @@ -47,7 +47,7 @@ } }, "dependencies": { - "@decky/ui": "^4.11.0", + "@decky/ui": "^4.11.4", "compare-versions": "^6.1.1", "filesize": "^10.1.2", "i18next": "^25.6.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 10da2f88..2b047aa8 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@decky/ui': - specifier: ^4.11.0 - version: 4.11.0 + specifier: ^4.11.4 + version: 4.11.4 compare-versions: specifier: ^6.1.1 version: 6.1.1 @@ -40,8 +40,8 @@ importers: version: 4.0.0 devDependencies: '@decky/api': - specifier: ^1.1.1 - version: 1.1.1 + specifier: ^1.1.3 + version: 1.1.3 '@rollup/plugin-commonjs': specifier: ^26.0.1 version: 26.0.1(rollup@4.22.4) @@ -219,11 +219,11 @@ packages: resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==} engines: {node: '>=6.9.0'} - '@decky/api@1.1.1': - resolution: {integrity: sha512-R5fkBRHBt5QIQY7Q0AlbVIhlIZ/nTzwBOoi8Rt4Go2fjFnoMKPInCJl6cPjXzimGwl2pyqKJgY6VnH6ar0XrHQ==} + '@decky/api@1.1.3': + resolution: {integrity: sha512-XsPCZxfxk5I1UtylIUN3qaWQI31siQbKfbLIskkI5innEatY1m4NQqBv/6hwPaO9mKMbdqYpnh5PSJDeMEOOBA==} - '@decky/ui@4.11.0': - resolution: {integrity: sha512-l9PstFC+S8FE8M2kIM78L8cYW4vzJ/ZD30II0huarHLcCsKM4Q+rbmEnbWjlJ1/KLmGXVRXBdAbyD4X/FzfxnQ==} + '@decky/ui@4.11.4': + resolution: {integrity: sha512-8rANkj5vkYTcT7VBBUzlBuowyBctU8gU5reWtsntmYdr7dGPLRqfgKDRqVH09HCd5plXyJKWDSpqiDsUHmKRJg==} '@esbuild/aix-ppc64@0.20.2': resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} @@ -2309,9 +2309,9 @@ snapshots: '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 - '@decky/api@1.1.1': {} + '@decky/api@1.1.3': {} - '@decky/ui@4.11.0': {} + '@decky/ui@4.11.4': {} '@esbuild/aix-ppc64@0.20.2': optional: true diff --git a/frontend/src/components/DeckyState.tsx b/frontend/src/components/DeckyState.tsx index d2ac63ae..d1b558c1 100644 --- a/frontend/src/components/DeckyState.tsx +++ b/frontend/src/components/DeckyState.tsx @@ -1,12 +1,14 @@ import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react'; import { DEFAULT_NOTIFICATION_SETTINGS, NotificationSettings } from '../notification-service'; -import { Plugin } from '../plugin'; +import { DisabledPlugin, Plugin } from '../plugin'; import { PluginUpdateMapping } from '../store'; import { VerInfo } from '../updater'; interface PublicDeckyState { plugins: Plugin[]; + disabledPlugins: DisabledPlugin[]; + installedPlugins: (Plugin | DisabledPlugin)[]; pluginOrder: string[]; frozenPlugins: string[]; hiddenPlugins: string[]; @@ -26,6 +28,8 @@ export interface UserInfo { export class DeckyState { private _plugins: Plugin[] = []; + private _disabledPlugins: DisabledPlugin[] = []; + private _installedPlugins: (Plugin | DisabledPlugin)[] = []; private _pluginOrder: string[] = []; private _frozenPlugins: string[] = []; private _hiddenPlugins: string[] = []; @@ -42,6 +46,8 @@ export class DeckyState { publicState(): PublicDeckyState { return { plugins: this._plugins, + disabledPlugins: this._disabledPlugins, + installedPlugins: this._installedPlugins, pluginOrder: this._pluginOrder, frozenPlugins: this._frozenPlugins, hiddenPlugins: this._hiddenPlugins, @@ -62,6 +68,13 @@ export class DeckyState { setPlugins(plugins: Plugin[]) { this._plugins = plugins; + this._installedPlugins = [...plugins, ...this._disabledPlugins]; + this.notifyUpdate(); + } + + setDisabledPlugins(disabledPlugins: DisabledPlugin[]) { + this._disabledPlugins = disabledPlugins; + this._installedPlugins = [...this._plugins, ...disabledPlugins]; this.notifyUpdate(); } @@ -125,6 +138,7 @@ interface DeckyStateContext extends PublicDeckyState { setIsLoaderUpdating(hasUpdate: boolean): void; setActivePlugin(name: string): void; setPluginOrder(pluginOrder: string[]): void; + setDisabledPlugins(disabled: DisabledPlugin[]): void; closeActivePlugin(): void; } @@ -163,6 +177,7 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) = const setActivePlugin = deckyState.setActivePlugin.bind(deckyState); const closeActivePlugin = deckyState.closeActivePlugin.bind(deckyState); const setPluginOrder = deckyState.setPluginOrder.bind(deckyState); + const setDisabledPlugins = deckyState.setDisabledPlugins.bind(deckyState); return ( <DeckyStateContext.Provider @@ -173,6 +188,7 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) = setActivePlugin, closeActivePlugin, setPluginOrder, + setDisabledPlugins, }} > {children} diff --git a/frontend/src/components/PluginView.tsx b/frontend/src/components/PluginView.tsx index 1d39972e..ffaa176a 100644 --- a/frontend/src/components/PluginView.tsx +++ b/frontend/src/components/PluginView.tsx @@ -1,7 +1,7 @@ import { ButtonItem, ErrorBoundary, Focusable, PanelSection, PanelSectionRow } from '@decky/ui'; import { FC, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { FaEyeSlash } from 'react-icons/fa'; +import { FaBan, FaEyeSlash } from 'react-icons/fa'; import { useDeckyState } from './DeckyState'; import NotificationBadge from './NotificationBadge'; @@ -9,8 +9,16 @@ import { useQuickAccessVisible } from './QuickAccessVisibleState'; import TitleView from './TitleView'; const PluginView: FC = () => { - const { plugins, hiddenPlugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = - useDeckyState(); + const { + plugins, + disabledPlugins, + hiddenPlugins, + updates, + activePlugin, + pluginOrder, + setActivePlugin, + closeActivePlugin, + } = useDeckyState(); const visible = useQuickAccessVisible(); const { t } = useTranslation(); @@ -21,7 +29,9 @@ const PluginView: FC = () => { .sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name)) .filter((p) => p.content) .filter(({ name }) => !hiddenPlugins.includes(name)); - }, [plugins, pluginOrder]); + }, [plugins, pluginOrder, hiddenPlugins]); + + const numberOfHidden = hiddenPlugins.filter((name) => !!plugins.find((p) => p.name === name)).length; if (activePlugin) { return ( @@ -53,12 +63,28 @@ const PluginView: FC = () => { </ButtonItem> </PanelSectionRow> ))} - {hiddenPlugins.length > 0 && ( - <div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem', marginTop: '10px' }}> - <FaEyeSlash /> - <div>{t('PluginView.hidden', { count: hiddenPlugins.length })}</div> - </div> - )} + <div + style={{ + display: 'flex', + flexDirection: 'column', + position: 'absolute', + justifyContent: 'center', + padding: '5px 0px', + }} + > + {numberOfHidden > 0 && ( + <div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem' }}> + <FaEyeSlash /> + <div>{t('PluginView.hidden', { count: numberOfHidden })}</div> + </div> + )} + {disabledPlugins.length > 0 && ( + <div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem' }}> + <FaBan /> + <div>{t('PluginView.disabled', { count: disabledPlugins.length })}</div> + </div> + )} + </div> </PanelSection> </div> </> diff --git a/frontend/src/components/TitleView.tsx b/frontend/src/components/TitleView.tsx index 0cb82b7f..cce779ff 100644 --- a/frontend/src/components/TitleView.tsx +++ b/frontend/src/components/TitleView.tsx @@ -8,7 +8,6 @@ import { useDeckyState } from './DeckyState'; const titleStyles: CSSProperties = { display: 'flex', - paddingTop: '3px', paddingRight: '16px', position: 'sticky', top: '0px', diff --git a/frontend/src/components/modals/MultiplePluginsInstallModal.tsx b/frontend/src/components/modals/MultiplePluginsInstallModal.tsx index 9c86f3db..e5c1c647 100644 --- a/frontend/src/components/modals/MultiplePluginsInstallModal.tsx +++ b/frontend/src/components/modals/MultiplePluginsInstallModal.tsx @@ -3,10 +3,11 @@ import { FC, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { FaCheck, FaDownload } from 'react-icons/fa'; -import { InstallType, InstallTypeTranslationMapping } from '../../plugin'; +import { DisabledPlugin, InstallType, InstallTypeTranslationMapping } from '../../plugin'; interface MultiplePluginsInstallModalProps { requests: { name: string; version: string; hash: string; install_type: InstallType }[]; + disabledPlugins: DisabledPlugin[]; onOK(): void | Promise<void>; onCancel(): void | Promise<void>; closeModal?(): void; @@ -17,6 +18,7 @@ type TitleTranslationMapping = 'mixed' | (typeof InstallTypeTranslationMapping)[ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({ requests, + disabledPlugins, onOK, onCancel, closeModal, @@ -116,10 +118,11 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({ version, }); + const disabled = disabledPlugins.some((p) => p.name === name); return ( <li key={i} style={{ display: 'flex', flexDirection: 'column' }}> <span> - {description}{' '} + {disabled ? `${description} - ${t('PluginInstallModal.disabled')}` : description}{' '} {(pluginsCompleted.includes(name) && <FaCheck />) || (name === pluginInProgress && <FaDownload />)} </span> {hash === 'False' && ( diff --git a/frontend/src/components/modals/PluginDisableModal.tsx b/frontend/src/components/modals/PluginDisableModal.tsx new file mode 100644 index 00000000..16ddd4bf --- /dev/null +++ b/frontend/src/components/modals/PluginDisableModal.tsx @@ -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; diff --git a/frontend/src/components/modals/PluginInstallModal.tsx b/frontend/src/components/modals/PluginInstallModal.tsx index 16419d91..0075fce5 100644 --- a/frontend/src/components/modals/PluginInstallModal.tsx +++ b/frontend/src/components/modals/PluginInstallModal.tsx @@ -9,6 +9,7 @@ interface PluginInstallModalProps { version: string; hash: string; installType: InstallType; + disabled?: boolean; onOK(): void; onCancel(): void; closeModal?(): void; @@ -19,6 +20,7 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ version, hash, installType, + disabled, onOK, onCancel, closeModal, @@ -45,6 +47,10 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ }, []); const installTypeTranslationKey = InstallTypeTranslationMapping[installType]; + const description = t(`PluginInstallModal.${installTypeTranslationKey}.desc`, { + artifact: artifact, + version: version, + }); return ( <ConfirmModal @@ -118,10 +124,7 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ // t('PluginInstallModal.update.desc') // t('PluginInstallModal.downgrade.desc') // t('PluginInstallModal.overwrite.desc') - t(`PluginInstallModal.${installTypeTranslationKey}.desc`, { - artifact: artifact, - version: version, - }) + disabled ? `${description} ${t('PluginInstallModal.disabled')}` : description } </div> {hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>} diff --git a/frontend/src/components/modals/PluginUninstallModal.tsx b/frontend/src/components/modals/PluginUninstallModal.tsx index be479859..37d3d789 100644 --- a/frontend/src/components/modals/PluginUninstallModal.tsx +++ b/frontend/src/components/modals/PluginUninstallModal.tsx @@ -2,8 +2,10 @@ import { ConfirmModal, Spinner } from '@decky/ui'; import { FC, useState } from 'react'; import { uninstallPlugin } from '../../plugin'; +import { DeckyState } from '../DeckyState'; interface PluginUninstallModalProps { + deckyState: DeckyState; name: string; title: string; buttonText: string; @@ -11,7 +13,14 @@ interface PluginUninstallModalProps { 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); return ( <ConfirmModal @@ -19,6 +28,7 @@ const PluginUninstallModal: FC<PluginUninstallModalProps> = ({ name, title, butt onOK={async () => { setUninstalling(true); await uninstallPlugin(name); + deckyState.setDisabledPlugins(deckyState.publicState().disabledPlugins.filter((d) => d.name !== name)); // uninstalling a plugin resets the hidden setting for it server-side // we invalidate here so if you re-install it, you won't have an out-of-date hidden filter await DeckyPluginLoader.frozenPluginsService.invalidate(); diff --git a/frontend/src/components/settings/pages/plugin_list/PluginListLabel.tsx b/frontend/src/components/settings/pages/plugin_list/PluginListLabel.tsx index fec03e56..59171b39 100644 --- a/frontend/src/components/settings/pages/plugin_list/PluginListLabel.tsx +++ b/frontend/src/components/settings/pages/plugin_list/PluginListLabel.tsx @@ -1,15 +1,16 @@ import { FC } from 'react'; import { useTranslation } from 'react-i18next'; -import { FaEyeSlash, FaLock } from 'react-icons/fa'; +import { FaBan, FaEyeSlash, FaLock } from 'react-icons/fa'; interface PluginListLabelProps { frozen: boolean; hidden: boolean; + disabled: boolean; name: string; version?: string; } -const PluginListLabel: FC<PluginListLabelProps> = ({ name, frozen, hidden, version }) => { +const PluginListLabel: FC<PluginListLabelProps> = ({ name, frozen, hidden, version, disabled }) => { const { t } = useTranslation(); return ( <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}> @@ -43,6 +44,20 @@ const PluginListLabel: FC<PluginListLabelProps> = ({ name, frozen, hidden, versi {t('PluginListLabel.hidden')} </div> )} + {disabled && ( + <div + style={{ + fontSize: '0.8rem', + color: '#dcdedf', + display: 'flex', + alignItems: 'center', + gap: '10px', + }} + > + <FaBan /> + {t('PluginListLabel.disabled')} + </div> + )} </div> ); }; diff --git a/frontend/src/components/settings/pages/plugin_list/index.tsx b/frontend/src/components/settings/pages/plugin_list/index.tsx index 9a7cb076..43d79709 100644 --- a/frontend/src/components/settings/pages/plugin_list/index.tsx +++ b/frontend/src/components/settings/pages/plugin_list/index.tsx @@ -2,9 +2,11 @@ import { DialogBody, DialogButton, DialogControlsSection, + Focusable, GamepadEvent, Menu, MenuItem, + NavEntryPositionPreferences, ReorderableEntry, ReorderableList, showContextMenu, @@ -13,7 +15,7 @@ import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { FaDownload, FaEllipsisH, FaRecycle } from 'react-icons/fa'; -import { InstallType } from '../../../../plugin'; +import { InstallType, enablePlugin } from '../../../../plugin'; import { StorePluginVersion, getPluginList, @@ -35,6 +37,7 @@ async function reinstallPlugin(pluginName: string, currentVersion?: string) { type PluginTableData = PluginData & { name: string; + disabled: boolean; frozen: boolean; onFreeze(): void; onUnfreeze(): void; @@ -54,22 +57,25 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> } 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) => { showContextMenu( <Menu label={t('PluginListIndex.plugin_actions')}> - <MenuItem - onSelected={async () => { - try { - await reloadPluginBackend(name); - } catch (err) { - console.error('Error Reloading Plugin Backend', err); - } - }} - > - {t('PluginListIndex.reload')} - </MenuItem> + {!disabled && ( + <MenuItem + onSelected={async () => { + try { + await reloadPluginBackend(name); + } catch (err) { + console.error('Error Reloading Plugin Backend', err); + } + }} + > + {t('PluginListIndex.reload')} + </MenuItem> + )} <MenuItem onSelected={() => DeckyPluginLoader.uninstallPlugin( @@ -82,11 +88,28 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> } > {t('PluginListIndex.uninstall')} </MenuItem> - {hidden ? ( - <MenuItem onSelected={onShow}>{t('PluginListIndex.show')}</MenuItem> + {disabled ? ( + <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 ? ( <MenuItem onSelected={onUnfreeze}>{t('PluginListIndex.unfreeze')}</MenuItem> ) : ( @@ -98,7 +121,7 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> } }; return ( - <> + <Focusable navEntryPreferPosition={NavEntryPositionPreferences.MAINTAIN_X} style={{ display: 'flex' }}> {update ? ( <DialogButton style={{ height: '40px', minWidth: '60px', marginRight: '10px' }} @@ -137,7 +160,7 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> } > <FaEllipsisH /> </DialogButton> - </> + </Focusable> ); } @@ -147,16 +170,18 @@ type PluginData = { }; 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[]>( 'pluginOrder', - plugins.map((plugin) => plugin.name), + installedPlugins.map((plugin) => plugin.name), ); const { t } = useTranslation(); useEffect(() => { DeckyPluginLoader.checkPluginUpdates(); - }, []); + }, [installedPlugins, frozenPlugins]); const [pluginEntries, setPluginEntries] = useState<ReorderableEntry<PluginTableData>[]>([]); const hiddenPluginsService = DeckyPluginLoader.hiddenPluginsService; @@ -164,15 +189,24 @@ export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) { useEffect(() => { setPluginEntries( - plugins.map(({ name, version }) => { + installedPlugins.map(({ name, version }) => { const frozen = frozenPlugins.includes(name); const hidden = hiddenPlugins.includes(name); 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), data: { name, + disabled: disabledPlugins.some((disabledPlugin) => disabledPlugin.name === name), frozen, hidden, 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 ( <div> <p>{t('PluginListIndex.no_plugin')}</p> diff --git a/frontend/src/components/settings/pages/testing/index.tsx b/frontend/src/components/settings/pages/testing/index.tsx index 8f02c207..0f8b5ebe 100644 --- a/frontend/src/components/settings/pages/testing/index.tsx +++ b/frontend/src/components/settings/pages/testing/index.tsx @@ -4,6 +4,7 @@ import { DialogControlsSection, Field, Focusable, + NavEntryPositionPreferences, Navigation, ProgressBar, SteamSpinner, @@ -65,9 +66,9 @@ export default function TestingVersionList() { if (testingVersions.length === 0) { return ( - <div> + <DialogBody> <p>No open PRs found</p> - </div> + </DialogBody> ); } @@ -79,7 +80,7 @@ export default function TestingVersionList() { <ul style={{ listStyleType: 'none', padding: '0' }}> {testingVersions.map((version) => { return ( - <li> + <li key={`${version.id}_${version.name}`}> <Field label={ <> @@ -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 style={{ height: '40px', minWidth: '60px', marginRight: '10px' }} onClick={async () => { diff --git a/frontend/src/components/store/PluginCard.tsx b/frontend/src/components/store/PluginCard.tsx index f64abd09..a47207c9 100644 --- a/frontend/src/components/store/PluginCard.tsx +++ b/frontend/src/components/store/PluginCard.tsx @@ -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 { useTranslation } from 'react-i18next'; 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 ExternalLink from '../ExternalLink'; interface PluginCardProps { storePlugin: StorePlugin; - installedPlugin: Plugin | undefined; + installedPlugin: Plugin | DisabledPlugin | undefined; } const PluginCard: FC<PluginCardProps> = ({ storePlugin, installedPlugin }) => { @@ -139,7 +147,10 @@ const PluginCard: FC<PluginCardProps> = ({ storePlugin, installedPlugin }) => { </div> <div className="deckyStoreCardButtonRow"> <PanelSectionRow> - <Focusable style={{ display: 'flex', gap: '5px', padding: 0 }}> + <Focusable + style={{ display: 'flex', gap: '5px', padding: 0 }} + navEntryPreferPosition={NavEntryPositionPreferences.MAINTAIN_X} + > <div className="deckyStoreCardInstallContainer" style={ diff --git a/frontend/src/components/store/Store.tsx b/frontend/src/components/store/Store.tsx index 3209ba08..f1d50033 100644 --- a/frontend/src/components/store/Store.tsx +++ b/frontend/src/components/store/Store.tsx @@ -105,7 +105,7 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }> })(); }, []); - const { plugins: installedPlugins } = useDeckyState(); + const { installedPlugins } = useDeckyState(); return ( <> @@ -240,6 +240,7 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }> }) .map((plugin: StorePlugin) => ( <PluginCard + key={`${plugin.id}_${plugin.name}`} storePlugin={plugin} installedPlugin={installedPlugins.find((installedPlugin) => installedPlugin.name === plugin.name)} /> diff --git a/frontend/src/index.ts b/frontend/src/index.ts index 4f4ff4f7..7ef5087f 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -22,11 +22,13 @@ DFLWebpack.findModule((m) => m.createPortal && m.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE); console.debug('[Decky:Boot] Setting up JSX internals...'); - const jsx = DFLWebpack.findModule((m) => m.jsx && Object.keys(m).length == 1)?.jsx; - if (jsx) { + 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, - jsxs: jsx, + jsx: jsxModule.jsx, + jsxs: jsxModule.jsx, Fragment: window.SP_REACT.Fragment, }; } diff --git a/frontend/src/plugin-loader.tsx b/frontend/src/plugin-loader.tsx index 2bdfcec1..fd4dc1c0 100644 --- a/frontend/src/plugin-loader.tsx +++ b/frontend/src/plugin-loader.tsx @@ -19,6 +19,7 @@ import { DeckyState, DeckyStateContextProvider, UserInfo, useDeckyState } from ' import { File, FileSelectionType } from './components/modals/filepicker'; import { deinitFilepickerPatches, initFilepickerPatches } from './components/modals/filepicker/patches'; import MultiplePluginsInstallModal from './components/modals/MultiplePluginsInstallModal'; +import PluginDisableModal from './components/modals/PluginDisableModal'; import PluginInstallModal from './components/modals/PluginInstallModal'; import PluginUninstallModal from './components/modals/PluginUninstallModal'; import NotificationBadge from './components/NotificationBadge'; @@ -30,7 +31,7 @@ import { FrozenPluginService } from './frozen-plugins-service'; import { HiddenPluginsService } from './hidden-plugins-service'; import Logger from './logger'; import { NotificationService } from './notification-service'; -import { InstallType, Plugin, PluginLoadType } from './plugin'; +import { DisabledPlugin, InstallType, Plugin, PluginLoadType } from './plugin'; import RouterHook from './router-hook'; import { deinitSteamFixes, initSteamFixes } from './steamfixes'; import { checkForPluginUpdates } from './store'; @@ -91,6 +92,7 @@ class PluginLoader extends Logger { DeckyBackend.addEventListener('loader/notify_updates', this.notifyUpdates.bind(this)); DeckyBackend.addEventListener('loader/import_plugin', this.importPlugin.bind(this)); DeckyBackend.addEventListener('loader/unload_plugin', this.unloadPlugin.bind(this)); + DeckyBackend.addEventListener('loader/disable_plugin', this.doDisablePlugin.bind(this)); DeckyBackend.addEventListener('loader/add_plugin_install_prompt', this.addPluginInstallPrompt.bind(this)); DeckyBackend.addEventListener( 'loader/add_multiple_plugins_install_prompt', @@ -175,7 +177,7 @@ class PluginLoader extends Logger { private getPluginsFromBackend = DeckyBackend.callable< [], - { name: string; version: string; load_type: PluginLoadType }[] + { name: string; version: string; load_type: PluginLoadType; disabled: boolean }[] >('loader/get_plugins'); private restartWebhelper = DeckyBackend.callable<[], void>('utilities/restart_webhelper'); @@ -198,10 +200,16 @@ class PluginLoader extends Logger { this.runCrashChecker(); const plugins = await this.getPluginsFromBackend(); const pluginLoadPromises = []; + const disabledPlugins: DisabledPlugin[] = []; const loadStart = performance.now(); for (const plugin of plugins) { - if (!this.hasPlugin(plugin.name)) - pluginLoadPromises.push(this.importPlugin(plugin.name, plugin.version, plugin.load_type, false)); + if (plugin.disabled) { + disabledPlugins.push({ name: plugin.name, version: plugin.version }); + this.deckyState.setDisabledPlugins(disabledPlugins); + } else { + if (!this.hasPlugin(plugin.name)) + pluginLoadPromises.push(this.importPlugin(plugin.name, plugin.version, plugin.load_type, false)); + } } await Promise.all(pluginLoadPromises); const loadEnd = performance.now(); @@ -252,7 +260,9 @@ class PluginLoader extends Logger { public async checkPluginUpdates() { 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); return updates; } @@ -290,6 +300,7 @@ class PluginLoader extends Logger { version={version} hash={hash} installType={install_type} + disabled={this.deckyState.publicState().disabledPlugins.some((p) => p.name === artifact)} onOK={() => DeckyBackend.call<[string]>('utilities/confirm_plugin_install', request_id)} onCancel={() => DeckyBackend.call<[string]>('utilities/cancel_plugin_install', request_id)} />, @@ -303,6 +314,7 @@ class PluginLoader extends Logger { showModal( <MultiplePluginsInstallModal requests={requests} + disabledPlugins={this.deckyState.publicState().disabledPlugins} onOK={() => DeckyBackend.call<[string]>('utilities/confirm_plugin_install', request_id)} onCancel={() => DeckyBackend.call<[string]>('utilities/cancel_plugin_install', request_id)} />, @@ -310,7 +322,19 @@ class PluginLoader extends Logger { } 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) { @@ -351,6 +375,19 @@ class PluginLoader extends Logger { this.errorBoundaryHook.deinit(); } + public doDisablePlugin(name: string) { + const plugin = this.plugins.find((plugin) => plugin.name === name); + if (plugin == undefined) return; + + plugin?.onDismount?.(); + this.plugins = this.plugins.filter((p) => p !== plugin); + this.deckyState.setDisabledPlugins([ + ...this.deckyState.publicState().disabledPlugins, + { name: plugin.name, version: plugin.version }, + ]); + this.deckyState.setPlugins(this.plugins); + } + public unloadPlugin(name: string, skipStateUpdate: boolean = false) { const plugin = this.plugins.find((plugin) => plugin.name === name); plugin?.onDismount?.(); @@ -363,6 +400,7 @@ class PluginLoader extends Logger { version?: string | undefined, loadType: PluginLoadType = PluginLoadType.ESMODULE_V1, useQueue: boolean = true, + timeoutMS?: number, ) { if (useQueue && this.reloadLock) { this.log('Reload currently in progress, adding to queue', name); @@ -376,9 +414,11 @@ class PluginLoader extends Logger { this.unloadPlugin(name, true); const startTime = performance.now(); - await this.importReactPlugin(name, version, loadType); + + await this.importReactPlugin(name, version, loadType, timeoutMS); const endTime = performance.now(); + this.deckyState.setDisabledPlugins(this.deckyState.publicState().disabledPlugins.filter((d) => d.name !== name)); this.deckyState.setPlugins(this.plugins); this.log(`Loaded ${name} in ${endTime - startTime}ms`); } catch (e) { @@ -388,7 +428,7 @@ class PluginLoader extends Logger { this.reloadLock = false; const nextPlugin = this.pluginReloadQueue.shift(); if (nextPlugin) { - this.importPlugin(nextPlugin.name, nextPlugin.version, loadType); + this.importPlugin(nextPlugin.name, nextPlugin.version, nextPlugin.loadType, true, timeoutMS); } } } @@ -398,12 +438,28 @@ class PluginLoader extends Logger { name: string, version?: string, loadType: PluginLoadType = PluginLoadType.ESMODULE_V1, + timeoutMS?: number, ) { let spExists = this.checkForSP(); + const timeoutException = new Error( + `${name} failed to load within ${timeoutMS ? `${timeoutMS / 1000} second` : ''} time limit`, + ); + let timeout: number | undefined; + try { switch (loadType) { case PluginLoadType.ESMODULE_V1: - const 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(); this.plugins.push({ @@ -415,12 +471,26 @@ class PluginLoader extends Logger { break; case PluginLoadType.LEGACY_EVAL_IIFE: - let res = await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, { - credentials: 'include', - headers: { - 'X-Decky-Auth': deckyAuthToken, - }, - }); + const fetchJS = async () => { + const controller = new AbortController(); + const { signal } = controller; + + if (timeoutMS !== undefined) timeout = setTimeout(() => controller.abort(), timeoutMS); + + try { + return await fetch(`http://127.0.0.1:1337/plugins/${name}/frontend_bundle`, { + credentials: 'include', + headers: { + 'X-Decky-Auth': deckyAuthToken, + }, + signal, + }); + } catch (e: any) { + throw 'name' in e && e.name === 'AbortError' ? timeoutException : e; + } + }; + + let res = await fetchJS(); if (res.ok) { let plugin_export: (serverAPI: any) => Plugin = await eval( (await res.text()) + `\n//# sourceURL=decky://decky/legacy_plugin/${encodeURIComponent(name)}/index.js`, @@ -439,6 +509,8 @@ class PluginLoader extends Logger { throw new Error(`${name} has no defined loadType.`); } } catch (e) { + if (e === timeoutException) throw timeoutException; + this.error('Error loading plugin ' + name, e); const TheError: FC<{}> = () => ( <PanelSection> @@ -481,6 +553,8 @@ class PluginLoader extends Logger { body: '' + e, icon: <FaExclamationCircle />, }); + } finally { + if (timeout !== undefined) clearTimeout(timeout); } if (spExists && !this.checkForSP()) { diff --git a/frontend/src/plugin.ts b/frontend/src/plugin.ts index f2b99f71..746ef29e 100644 --- a/frontend/src/plugin.ts +++ b/frontend/src/plugin.ts @@ -15,6 +15,8 @@ export interface Plugin { titleView?: JSX.Element; } +export type DisabledPlugin = Pick<Plugin, 'name' | 'version'>; + export enum InstallType { INSTALL, REINSTALL, @@ -56,3 +58,5 @@ type installPluginsArgs = [ export let installPlugins = DeckyBackend.callable<installPluginsArgs>('utilities/install_plugins'); export let uninstallPlugin = DeckyBackend.callable<[name: string]>('utilities/uninstall_plugin'); +export let enablePlugin = DeckyBackend.callable<[name: string]>('utilities/enable_plugin'); +export let disablePlugin = DeckyBackend.callable<[name: string]>('utilities/disable_plugin'); diff --git a/frontend/src/store.tsx b/frontend/src/store.tsx index dfd9b04b..33c384a5 100644 --- a/frontend/src/store.tsx +++ b/frontend/src/store.tsx @@ -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'; 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 updateMap = new Map<string, StorePluginVersion>(); for (let plugin of plugins) { const remotePlugin = serverData?.find((x) => x.name == plugin.name); //FIXME: Ugly hack since plugin.version might be null during evaluation, //so this will set the older version possible - const curVer = plugin.version ? plugin.version : '0.0'; + const curVer = plugin.version ? plugin.version : '0.0.0'; + if ( remotePlugin && remotePlugin.versions?.length > 0 && plugin.version != remotePlugin?.versions?.[0]?.name && + validate(remotePlugin.versions?.[0]?.name) && + validate(curVer) && compare(remotePlugin?.versions?.[0]?.name, curVer, '>') ) { updateMap.set(plugin.name, remotePlugin.versions[0]); diff --git a/frontend/src/tabs-hook.tsx b/frontend/src/tabs-hook.tsx index a70c6580..5d9518fd 100644 --- a/frontend/src/tabs-hook.tsx +++ b/frontend/src/tabs-hook.tsx @@ -29,7 +29,8 @@ interface Tab { class TabsHook extends Logger { // private keys = 7; tabs: Tab[] = []; - private qamPatch?: Patch; + private qamBrowserViewPatch?: Patch; + private qamEmbeddedPatch?: Patch; constructor() { super('TabsHook'); @@ -40,11 +41,13 @@ class TabsHook extends Logger { } 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 qamRenderer = Object.values(qamModule).find((e: any) => + const qamBrowserViewRenderer = Object.values(qamModule).find((e: any) => e?.type?.toString?.()?.includes('QuickAccessMenuBrowserView'), ); + const qamEmbeddedRenderer = Object.values(qamModule).find((e: any) => + e?.type?.toString?.()?.includes('QuickAccessMenuEmbedded'), + ); const patchHandler = createReactTreePatcher( [(tree) => findInReactTree(tree, (node) => node?.props?.onFocusNavDeactivated)], @@ -56,12 +59,21 @@ class TabsHook extends Logger { '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 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) { + console.log('patching existing qam'); // Only affects this fiber node so we don't need to unpatch here qamNode.type = qamNode.elementType.type; if (qamNode?.alternate) { @@ -71,7 +83,8 @@ class TabsHook extends Logger { } deinit() { - this.qamPatch?.unpatch(); + this.qamBrowserViewPatch?.unpatch(); + this.qamEmbeddedPatch?.unpatch(); } add(tab: Tab) { diff --git a/frontend/src/toaster.tsx b/frontend/src/toaster.tsx index 2305b870..99f8023e 100644 --- a/frontend/src/toaster.tsx +++ b/frontend/src/toaster.tsx @@ -81,6 +81,7 @@ class Toaster extends Logger { const info = { showToast: toast.showToast, sound: toast.sound, + playSound: toast.playSound, eFeature: 0, toastDurationMS: toastData.nToastDurationMS, bCritical: toast.critical, |
