From 47bc910a8482d3d2cc882e28e862ca5ad61063b6 Mon Sep 17 00:00:00 2001 From: Jonas Dellinger Date: Wed, 7 Jun 2023 07:35:05 +0200 Subject: Add functionality to hide plugins from quick access menu (#468) --- frontend/src/components/DeckyState.tsx | 18 ++++-- frontend/src/components/PluginView.tsx | 11 ++++ .../src/components/modals/PluginUninstallModal.tsx | 30 +++++++++ .../settings/pages/plugin_list/PluginListLabel.tsx | 34 ++++++++++ .../settings/pages/plugin_list/index.tsx | 74 +++++++++++++--------- frontend/src/hidden-plugins-service.tsx | 34 ++++++++++ frontend/src/plugin-loader.tsx | 23 ++----- 7 files changed, 174 insertions(+), 50 deletions(-) create mode 100644 frontend/src/components/modals/PluginUninstallModal.tsx create mode 100644 frontend/src/components/settings/pages/plugin_list/PluginListLabel.tsx create mode 100644 frontend/src/hidden-plugins-service.tsx (limited to 'frontend/src') diff --git a/frontend/src/components/DeckyState.tsx b/frontend/src/components/DeckyState.tsx index 67d4b78e..53ef6d2d 100644 --- a/frontend/src/components/DeckyState.tsx +++ b/frontend/src/components/DeckyState.tsx @@ -7,6 +7,7 @@ import { VerInfo } from '../updater'; interface PublicDeckyState { plugins: Plugin[]; pluginOrder: string[]; + hiddenPlugins: string[]; activePlugin: Plugin | null; updates: PluginUpdateMapping | null; hasLoaderUpdate?: boolean; @@ -17,6 +18,7 @@ interface PublicDeckyState { export class DeckyState { private _plugins: Plugin[] = []; private _pluginOrder: string[] = []; + private _hiddenPlugins: string[] = []; private _activePlugin: Plugin | null = null; private _updates: PluginUpdateMapping | null = null; private _hasLoaderUpdate: boolean = false; @@ -29,6 +31,7 @@ export class DeckyState { return { plugins: this._plugins, pluginOrder: this._pluginOrder, + hiddenPlugins: this._hiddenPlugins, activePlugin: this._activePlugin, updates: this._updates, hasLoaderUpdate: this._hasLoaderUpdate, @@ -52,6 +55,11 @@ export class DeckyState { this.notifyUpdate(); } + setHiddenPlugins(hiddenPlugins: string[]) { + this._hiddenPlugins = hiddenPlugins; + this.notifyUpdate(); + } + setActivePlugin(name: string) { this._activePlugin = this._plugins.find((plugin) => plugin.name === name) ?? null; this.notifyUpdate(); @@ -111,11 +119,11 @@ export const DeckyStateContextProvider: FC = ({ children, deckyState }) = return () => deckyState.eventBus.removeEventListener('update', onUpdate); }, []); - const setIsLoaderUpdating = (hasUpdate: boolean) => deckyState.setIsLoaderUpdating(hasUpdate); - const setVersionInfo = (versionInfo: VerInfo) => deckyState.setVersionInfo(versionInfo); - const setActivePlugin = (name: string) => deckyState.setActivePlugin(name); - const closeActivePlugin = () => deckyState.closeActivePlugin(); - const setPluginOrder = (pluginOrder: string[]) => deckyState.setPluginOrder(pluginOrder); + const setIsLoaderUpdating = deckyState.setIsLoaderUpdating.bind(deckyState); + const setVersionInfo = deckyState.setVersionInfo.bind(deckyState); + const setActivePlugin = deckyState.setActivePlugin.bind(deckyState); + const closeActivePlugin = deckyState.closeActivePlugin.bind(deckyState); + const setPluginOrder = deckyState.setPluginOrder.bind(deckyState); return ( { + const { hiddenPlugins } = useDeckyState(); const { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState(); const visible = useQuickAccessVisible(); + const { t } = useTranslation(); const [pluginList, setPluginList] = useState( plugins.sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name)), @@ -48,6 +52,7 @@ const PluginView: VFC = () => { {pluginList .filter((p) => p.content) + .filter(({ name }) => !hiddenPlugins.includes(name)) .map(({ name, icon }) => ( setActivePlugin(name)}> @@ -59,6 +64,12 @@ const PluginView: VFC = () => { ))} + {hiddenPlugins.length > 0 && ( +
+ +
{t('PluginView.hidden', { count: hiddenPlugins.length })}
+
+ )}
diff --git a/frontend/src/components/modals/PluginUninstallModal.tsx b/frontend/src/components/modals/PluginUninstallModal.tsx new file mode 100644 index 00000000..e7ecbc99 --- /dev/null +++ b/frontend/src/components/modals/PluginUninstallModal.tsx @@ -0,0 +1,30 @@ +import { ConfirmModal } from 'decky-frontend-lib'; +import { FC } from 'react'; + +interface PluginUninstallModalProps { + name: string; + title: string; + buttonText: string; + description: string; + closeModal?(): void; +} + +const PluginUninstallModal: FC = ({ name, title, buttonText, description, closeModal }) => { + return ( + { + await window.DeckyPluginLoader.callServerMethod('uninstall_plugin', { name }); + // uninstalling a plugin resets the hidden setting for it server-side + // we invalidate here so if you re-install it, you won't have an out-of-date hidden filter + await window.DeckyPluginLoader.hiddenPluginsService.invalidate(); + }} + strTitle={title} + strOKButtonText={buttonText} + > + {description} + + ); +}; + +export default PluginUninstallModal; diff --git a/frontend/src/components/settings/pages/plugin_list/PluginListLabel.tsx b/frontend/src/components/settings/pages/plugin_list/PluginListLabel.tsx new file mode 100644 index 00000000..a49f808f --- /dev/null +++ b/frontend/src/components/settings/pages/plugin_list/PluginListLabel.tsx @@ -0,0 +1,34 @@ +import { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { FaEyeSlash } from 'react-icons/fa'; + +interface PluginListLabelProps { + hidden: boolean; + name: string; + version?: string; +} + +const PluginListLabel: FC = ({ name, hidden, version }) => { + const { t } = useTranslation(); + return ( +
+
{version ? `${name} - ${version}` : name}
+ {hidden && ( +
+ + {t('PluginListLabel.hidden')} +
+ )} +
+ ); +}; + +export default PluginListLabel; diff --git a/frontend/src/components/settings/pages/plugin_list/index.tsx b/frontend/src/components/settings/pages/plugin_list/index.tsx index fab8ec2b..09d06d48 100644 --- a/frontend/src/components/settings/pages/plugin_list/index.tsx +++ b/frontend/src/components/settings/pages/plugin_list/index.tsx @@ -22,10 +22,7 @@ import { } from '../../../../store'; import { useSetting } from '../../../../utils/hooks/useSetting'; import { useDeckyState } from '../../../DeckyState'; - -function labelToName(pluginLabel: string, pluginVersion?: string): string { - return pluginVersion ? pluginLabel.substring(0, pluginLabel.indexOf(` - ${pluginVersion}`)) : pluginLabel; -} +import PluginListLabel from './PluginListLabel'; async function reinstallPlugin(pluginName: string, currentVersion?: string) { const serverData = await getPluginList(); @@ -36,10 +33,17 @@ async function reinstallPlugin(pluginName: string, currentVersion?: string) { } } -function PluginInteractables(props: { entry: ReorderableEntry }) { - const data = props.entry.data; +type PluginTableData = PluginData & { name: string; hidden: boolean; onHide(): void; onShow(): void }; + +function PluginInteractables(props: { entry: ReorderableEntry }) { const { t } = useTranslation(); - let pluginName = labelToName(props.entry.label, data?.version); + + // nothing to display without this data... + if (!props.entry.data) { + return null; + } + + const { name, update, version, onHide, onShow, hidden } = props.entry.data; const showCtxMenu = (e: MouseEvent | GamepadEvent) => { showContextMenu( @@ -47,7 +51,7 @@ function PluginInteractables(props: { entry: ReorderableEntry }) { { try { - fetch(`http://127.0.0.1:1337/plugins/${pluginName}/reload`, { + fetch(`http://127.0.0.1:1337/plugins/${name}/reload`, { method: 'POST', credentials: 'include', headers: { @@ -58,7 +62,7 @@ function PluginInteractables(props: { entry: ReorderableEntry }) { console.error('Error Reloading Plugin Backend', err); } - window.DeckyPluginLoader.importPlugin(pluginName, data?.version); + window.DeckyPluginLoader.importPlugin(name, version); }} > {t('PluginListIndex.reload')} @@ -66,15 +70,20 @@ function PluginInteractables(props: { entry: ReorderableEntry }) { window.DeckyPluginLoader.uninstallPlugin( - pluginName, - t('PluginLoader.plugin_uninstall.title', { name: pluginName }), + name, + t('PluginLoader.plugin_uninstall.title', { name }), t('PluginLoader.plugin_uninstall.button'), - t('PluginLoader.plugin_uninstall.desc', { name: pluginName }), + t('PluginLoader.plugin_uninstall.desc', { name }), ) } > {t('PluginListIndex.uninstall')} + {hidden ? ( + {t('PluginListIndex.show')} + ) : ( + {t('PluginListIndex.hide')} + )} , e.currentTarget ?? window, ); @@ -82,22 +91,22 @@ function PluginInteractables(props: { entry: ReorderableEntry }) { return ( <> - {data?.update ? ( + {update ? ( requestPluginInstall(pluginName, data?.update as StorePluginVersion, InstallType.UPDATE)} - onOKButton={() => requestPluginInstall(pluginName, data?.update as StorePluginVersion, InstallType.UPDATE)} + onClick={() => requestPluginInstall(name, update, InstallType.UPDATE)} + onOKButton={() => requestPluginInstall(name, update, InstallType.UPDATE)} >
- {t('PluginListIndex.update_to', { name: data?.update?.name })} + {t('PluginListIndex.update_to', { name: update.name })}
) : ( reinstallPlugin(pluginName, data?.version)} - onOKButton={() => reinstallPlugin(pluginName, data?.version)} + onClick={() => reinstallPlugin(name, version)} + onOKButton={() => reinstallPlugin(name, version)} >
{t('PluginListIndex.reinstall')} @@ -130,7 +139,7 @@ type PluginData = { }; export default function PluginList() { - const { plugins, updates, pluginOrder, setPluginOrder } = useDeckyState(); + const { plugins, updates, pluginOrder, setPluginOrder, hiddenPlugins } = useDeckyState(); const [_, setPluginOrderSetting] = useSetting( 'pluginOrder', plugins.map((plugin) => plugin.name), @@ -141,22 +150,29 @@ export default function PluginList() { window.DeckyPluginLoader.checkPluginUpdates(); }, []); - const [pluginEntries, setPluginEntries] = useState[]>([]); + const [pluginEntries, setPluginEntries] = useState[]>([]); + const hiddenPluginsService = window.DeckyPluginLoader.hiddenPluginsService; useEffect(() => { setPluginEntries( - plugins.map((plugin) => { + plugins.map(({ name, version }) => { + const hidden = hiddenPlugins.includes(name); + return { - label: plugin.version ? `${plugin.name} - ${plugin.version}` : plugin.name, + label: