mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-07-11 23:21:57 +00:00
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>
This commit is contained in:
@@ -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[];
|
||||||
|
disabled: 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,
|
||||||
|
disabled: this._disabledPlugins,
|
||||||
|
installedPlugins: this._disabledPlugins,
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { ConfirmModal, Spinner } from '@decky/ui';
|
||||||
|
import { FC, useState } from 'react';
|
||||||
|
|
||||||
|
import { disablePlugin, uninstallPlugin } 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;
|
||||||
@@ -35,6 +35,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,7 +55,7 @@ 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(
|
||||||
@@ -82,6 +83,22 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
|
|||||||
>
|
>
|
||||||
{t('PluginListIndex.uninstall')}
|
{t('PluginListIndex.uninstall')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{disabled ? <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> :
|
||||||
|
// implement enabler
|
||||||
|
<>
|
||||||
|
</>
|
||||||
|
}
|
||||||
{hidden ? (
|
{hidden ? (
|
||||||
<MenuItem onSelected={onShow}>{t('PluginListIndex.show')}</MenuItem>
|
<MenuItem onSelected={onShow}>{t('PluginListIndex.show')}</MenuItem>
|
||||||
) : (
|
) : (
|
||||||
@@ -147,10 +164,11 @@ 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, disabled, 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();
|
||||||
|
|
||||||
@@ -164,7 +182,7 @@ 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);
|
||||||
|
|
||||||
@@ -173,6 +191,7 @@ export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) {
|
|||||||
position: pluginOrder.indexOf(name),
|
position: pluginOrder.indexOf(name),
|
||||||
data: {
|
data: {
|
||||||
name,
|
name,
|
||||||
|
disabled: disabled.some(disabledPlugin => disabledPlugin.name === name),
|
||||||
frozen,
|
frozen,
|
||||||
hidden,
|
hidden,
|
||||||
isDeveloper,
|
isDeveloper,
|
||||||
@@ -186,9 +205,9 @@ export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) {
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}, [plugins, updates, hiddenPlugins]);
|
}, [installedPlugins, updates, hiddenPlugins]);
|
||||||
|
|
||||||
if (plugins.length === 0) {
|
if (installedPlugins.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>{t('PluginListIndex.no_plugin')}</p>
|
<p>{t('PluginListIndex.no_plugin')}</p>
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ 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 }) => {
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }>
|
|||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { plugins: installedPlugins } = useDeckyState();
|
const { installedPlugins } = useDeckyState();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -30,7 +30,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';
|
||||||
@@ -197,7 +197,7 @@ class PluginLoader extends Logger {
|
|||||||
|
|
||||||
private getPluginsFromBackend = DeckyBackend.callable<
|
private getPluginsFromBackend = DeckyBackend.callable<
|
||||||
[],
|
[],
|
||||||
{ name: string; version: string; load_type: PluginLoadType }[]
|
{ name: string; version: string; load_type: PluginLoadType; disabled: boolean }[]
|
||||||
>('loader/get_plugins');
|
>('loader/get_plugins');
|
||||||
|
|
||||||
private restartWebhelper = DeckyBackend.callable<[], void>('utilities/restart_webhelper');
|
private restartWebhelper = DeckyBackend.callable<[], void>('utilities/restart_webhelper');
|
||||||
@@ -220,10 +220,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();
|
||||||
@@ -335,6 +341,10 @@ class PluginLoader extends Logger {
|
|||||||
showModal(<PluginUninstallModal name={name} title={title} buttonText={buttonText} description={description} />);
|
showModal(<PluginUninstallModal name={name} title={title} buttonText={buttonText} description={description} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public disablePlugin(name: string, title: string, buttonText: string, description: string) {
|
||||||
|
showModal(<PluginUninstallModal name={name} title={title} buttonText={buttonText} description={description} />);
|
||||||
|
}
|
||||||
|
|
||||||
public hasPlugin(name: string) {
|
public hasPlugin(name: string) {
|
||||||
return Boolean(this.plugins.find((plugin) => plugin.name == name));
|
return Boolean(this.plugins.find((plugin) => plugin.name == name));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export interface Plugin {
|
|||||||
titleView?: JSX.Element;
|
titleView?: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DisabledPlugin = Pick<Plugin, 'name' | 'version'>;
|
||||||
|
|
||||||
export enum InstallType {
|
export enum InstallType {
|
||||||
INSTALL,
|
INSTALL,
|
||||||
REINSTALL,
|
REINSTALL,
|
||||||
@@ -55,3 +57,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');
|
||||||
|
|||||||
Reference in New Issue
Block a user