mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-13 12:15:09 +03:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 968c8b113c | |||
| 6fac72112c | |||
| 6640279ab3 | |||
| d2b6999520 | |||
| 6fba20239b | |||
| c3c0e87c6f | |||
| 1ae6519209 | |||
| 65f1eb052d |
Vendored
+2
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"deckip" : "0.0.0.0",
|
||||
"deckip" : "192.168.1.69",
|
||||
"deckport" : "22",
|
||||
"deckuser" : "deck",
|
||||
"deckpass" : "ssap",
|
||||
"deckpass" : "deck",
|
||||
"deckkey" : "-i ${env:HOME}/.ssh/id_rsa",
|
||||
"deckdir" : "/home/deck"
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,22 @@ 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):
|
||||
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)
|
||||
|
||||
plugin = self.context.plugin_loader.plugins[name]
|
||||
plugin.start()
|
||||
await self.context.plugin_loader.dispatch_plugin(plugin.name, plugin.version, plugin.load_type)
|
||||
|
||||
@@ -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[];
|
||||
disabled: 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,
|
||||
disabled: 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ConfirmModal, Spinner } from '@decky/ui';
|
||||
import { FC, useState } from 'react';
|
||||
|
||||
import { disablePlugin } from '../../plugin';
|
||||
|
||||
interface PluginUninstallModalProps {
|
||||
name: string;
|
||||
title: string;
|
||||
buttonText: string;
|
||||
description: string;
|
||||
closeModal?(): void;
|
||||
}
|
||||
|
||||
const PluginDisableModal: FC<PluginUninstallModalProps> = ({ name, title, buttonText, description, closeModal }) => {
|
||||
const [disabling, setDisabling] = useState<boolean>(false);
|
||||
return (
|
||||
<ConfirmModal
|
||||
closeModal={closeModal}
|
||||
onOK={async () => {
|
||||
setDisabling(true);
|
||||
await disablePlugin(name);
|
||||
|
||||
//not sure about this yet
|
||||
|
||||
// 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();
|
||||
await DeckyPluginLoader.hiddenPluginsService.invalidate();
|
||||
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;
|
||||
@@ -1,15 +1,16 @@
|
||||
import { FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaEyeSlash, FaLock } from 'react-icons/fa';
|
||||
import { FaEyeSlash, FaLock, FaMoon } 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',
|
||||
}}
|
||||
>
|
||||
<FaMoon />
|
||||
{t('PluginListLabel.disabled')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaDownload, FaEllipsisH, FaRecycle } from 'react-icons/fa';
|
||||
|
||||
import { InstallType } from '../../../../plugin';
|
||||
import { enablePlugin, InstallType } from '../../../../plugin';
|
||||
import {
|
||||
StorePluginVersion,
|
||||
getPluginList,
|
||||
@@ -35,6 +35,7 @@ async function reinstallPlugin(pluginName: string, currentVersion?: string) {
|
||||
|
||||
type PluginTableData = PluginData & {
|
||||
name: string;
|
||||
disabled: boolean;
|
||||
frozen: boolean;
|
||||
onFreeze(): void;
|
||||
onUnfreeze(): void;
|
||||
@@ -54,7 +55,7 @@ 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(
|
||||
@@ -82,6 +83,25 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
|
||||
>
|
||||
{t('PluginListIndex.uninstall')}
|
||||
</MenuItem>
|
||||
{disabled ?
|
||||
<MenuItem
|
||||
onSelected={() => enablePlugin(name)}
|
||||
>
|
||||
{t('PluginListIndex.plugin_enable')}
|
||||
</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.plugin_disable')}
|
||||
</MenuItem>
|
||||
}
|
||||
{hidden ? (
|
||||
<MenuItem onSelected={onShow}>{t('PluginListIndex.show')}</MenuItem>
|
||||
) : (
|
||||
@@ -147,10 +167,11 @@ type PluginData = {
|
||||
};
|
||||
|
||||
export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) {
|
||||
const { plugins, updates, pluginOrder, setPluginOrder, frozenPlugins, hiddenPlugins } = useDeckyState();
|
||||
const { installedPlugins, disabled, updates, pluginOrder, setPluginOrder, frozenPlugins, hiddenPlugins } = useDeckyState();
|
||||
|
||||
const [_, setPluginOrderSetting] = useSetting<string[]>(
|
||||
'pluginOrder',
|
||||
plugins.map((plugin) => plugin.name),
|
||||
installedPlugins.map((plugin) => plugin.name),
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -164,15 +185,17 @@ 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={disabled.find(p => p.name == name) !== undefined} />,
|
||||
position: pluginOrder.indexOf(name),
|
||||
data: {
|
||||
name,
|
||||
disabled: disabled.some(disabledPlugin => disabledPlugin.name === name),
|
||||
frozen,
|
||||
hidden,
|
||||
isDeveloper,
|
||||
@@ -186,9 +209,9 @@ export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) {
|
||||
};
|
||||
}),
|
||||
);
|
||||
}, [plugins, updates, hiddenPlugins]);
|
||||
}, [installedPlugins, updates, hiddenPlugins]);
|
||||
|
||||
if (plugins.length === 0) {
|
||||
if (installedPlugins.length === 0) {
|
||||
return (
|
||||
<div>
|
||||
<p>{t('PluginListIndex.no_plugin')}</p>
|
||||
|
||||
@@ -3,13 +3,13 @@ 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 }) => {
|
||||
|
||||
@@ -105,7 +105,7 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }>
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const { plugins: installedPlugins } = useDeckyState();
|
||||
const { installedPlugins } = useDeckyState();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -30,7 +30,7 @@ import { FrozenPluginService } from './frozen-plugins-service';
|
||||
import { HiddenPluginsService } from './hidden-plugins-service';
|
||||
import Logger from './logger';
|
||||
import { NotificationService } from './notification-service';
|
||||
import { 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';
|
||||
@@ -39,6 +39,7 @@ import Toaster from './toaster';
|
||||
import { getVersionInfo } from './updater';
|
||||
import { getSetting, setSetting } from './utils/settings';
|
||||
import TranslationHelper, { TranslationClass } from './utils/TranslationHelper';
|
||||
import PluginDisablelModal from './components/modals/PluginDisablelModal';
|
||||
|
||||
const StorePage = lazy(() => import('./components/store/Store'));
|
||||
const SettingsPage = lazy(() => import('./components/settings'));
|
||||
@@ -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',
|
||||
@@ -197,7 +199,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');
|
||||
@@ -206,10 +208,10 @@ class PluginLoader extends Logger {
|
||||
let registration: any;
|
||||
const uiMode = await new Promise(
|
||||
(r) =>
|
||||
(registration = SteamClient.UI.RegisterForUIModeChanged((mode: EUIMode) => {
|
||||
r(mode);
|
||||
registration.unregister();
|
||||
})),
|
||||
(registration = SteamClient.UI.RegisterForUIModeChanged((mode: EUIMode) => {
|
||||
r(mode);
|
||||
registration.unregister();
|
||||
})),
|
||||
);
|
||||
if (uiMode == EUIMode.GamePad) {
|
||||
// wait for SP window to exist before loading plugins
|
||||
@@ -220,10 +222,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();
|
||||
@@ -335,6 +343,10 @@ class PluginLoader extends Logger {
|
||||
showModal(<PluginUninstallModal name={name} title={title} buttonText={buttonText} description={description} />);
|
||||
}
|
||||
|
||||
public disablePlugin(name: string, title: string, buttonText: string, description: string) {
|
||||
showModal(<PluginDisablelModal name={name} title={title} buttonText={buttonText} description={description} />);
|
||||
}
|
||||
|
||||
public hasPlugin(name: string) {
|
||||
return Boolean(this.plugins.find((plugin) => plugin.name == name));
|
||||
}
|
||||
@@ -373,6 +385,17 @@ 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().disabled,
|
||||
{ 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?.();
|
||||
@@ -392,12 +415,15 @@ class PluginLoader extends Logger {
|
||||
return;
|
||||
}
|
||||
|
||||
this.deckyState.setDisabledPlugins(this.deckyState.publicState().disabled.filter(d => d.name !== name))
|
||||
|
||||
try {
|
||||
if (useQueue) this.reloadLock = true;
|
||||
this.log(`Trying to load ${name}`);
|
||||
|
||||
this.unloadPlugin(name, true);
|
||||
const startTime = performance.now();
|
||||
|
||||
await this.importReactPlugin(name, version, loadType);
|
||||
const endTime = performance.now();
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface Plugin {
|
||||
titleView?: JSX.Element;
|
||||
}
|
||||
|
||||
export type DisabledPlugin = Pick<Plugin, 'name' | 'version'>;
|
||||
|
||||
export enum InstallType {
|
||||
INSTALL,
|
||||
REINSTALL,
|
||||
@@ -55,3 +57,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');
|
||||
|
||||
Reference in New Issue
Block a user