Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .vscode/defsettings.json
Original file line number Diff line number Diff line change
@@ -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"
}
7 changes: 6 additions & 1 deletion backend/decky_loader/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def __init__(self, server_instance: PluginManager, ws: WSRouter, plugin_path: st
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()
Expand Down Expand Up @@ -130,7 +131,7 @@ async def handle_frontend_locales(self, request: web.Request):

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"]]
Expand Down Expand Up @@ -164,6 +165,10 @@ async def plugin_emitted_event(event: str, args: Any):
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")
Expand Down
1 change: 1 addition & 0 deletions backend/decky_loader/plugin/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def __init__(self, file: str, plugin_directory: str, plugin_path: str, emit_call
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)

Expand Down
24 changes: 22 additions & 2 deletions backend/decky_loader/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ def __init__(self, context: PluginManager) -> None:
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)
Expand Down Expand Up @@ -214,7 +216,7 @@ async def http_request(self, req: Request) -> StreamResponse:

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,
Expand Down Expand Up @@ -390,7 +392,6 @@ async def filepicker_ls(self,
"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):
Expand Down Expand Up @@ -474,3 +475,22 @@ async def get_user_info(self) -> Dict[str, str]:

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)
15 changes: 14 additions & 1 deletion frontend/src/components/DeckyState.tsx
Original file line number Diff line number Diff line change
@@ -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[];
Expand All @@ -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[] = [];
Expand All @@ -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,
Expand All @@ -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();
}

Expand Down
46 changes: 46 additions & 0 deletions frontend/src/components/modals/PluginDisablelModal.tsx
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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' }}>
Expand Down Expand Up @@ -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>
);
};
Expand Down
39 changes: 31 additions & 8 deletions frontend/src/components/settings/pages/plugin_list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -35,6 +35,7 @@ async function reinstallPlugin(pluginName: string, currentVersion?: string) {

type PluginTableData = PluginData & {
name: string;
disabled: boolean;
frozen: boolean;
onFreeze(): void;
onUnfreeze(): void;
Expand All @@ -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(
Expand Down Expand Up @@ -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>
) : (
Expand Down Expand Up @@ -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();

Expand All @@ -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,
Expand All @@ -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>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/store/PluginCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/store/Store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }>
})();
}, []);

const { plugins: installedPlugins } = useDeckyState();
const { installedPlugins } = useDeckyState();

return (
<>
Expand Down
Loading
Loading