mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-17 16:57:50 +00:00
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>
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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,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 () => {
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }>
|
|||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { plugins: installedPlugins } = useDeckyState();
|
const { installedPlugins } = useDeckyState();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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',
|
||||||
@@ -175,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');
|
||||||
@@ -198,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();
|
||||||
@@ -252,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;
|
||||||
}
|
}
|
||||||
@@ -290,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)}
|
||||||
/>,
|
/>,
|
||||||
@@ -303,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)}
|
||||||
/>,
|
/>,
|
||||||
@@ -310,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) {
|
||||||
@@ -351,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?.();
|
||||||
@@ -363,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);
|
||||||
@@ -376,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) {
|
||||||
@@ -388,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -398,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({
|
||||||
@@ -415,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`,
|
||||||
@@ -439,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>
|
||||||
@@ -481,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()) {
|
||||||
|
|||||||
@@ -15,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,
|
||||||
@@ -56,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');
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
Reference in New Issue
Block a user