mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-17 00:37:49 +00:00
Add functionality to hide plugins from quick access menu (#468)
This commit is contained in:
@@ -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<Props> = ({ 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 (
|
||||
<DeckyStateContext.Provider
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
staticClasses,
|
||||
} from 'decky-frontend-lib';
|
||||
import { VFC, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaEyeSlash } from 'react-icons/fa';
|
||||
|
||||
import { Plugin } from '../plugin';
|
||||
import { useDeckyState } from './DeckyState';
|
||||
@@ -16,8 +18,10 @@ import { useQuickAccessVisible } from './QuickAccessVisibleState';
|
||||
import TitleView from './TitleView';
|
||||
|
||||
const PluginView: VFC = () => {
|
||||
const { hiddenPlugins } = useDeckyState();
|
||||
const { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState();
|
||||
const visible = useQuickAccessVisible();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [pluginList, setPluginList] = useState<Plugin[]>(
|
||||
plugins.sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name)),
|
||||
@@ -48,6 +52,7 @@ const PluginView: VFC = () => {
|
||||
<PanelSection>
|
||||
{pluginList
|
||||
.filter((p) => p.content)
|
||||
.filter(({ name }) => !hiddenPlugins.includes(name))
|
||||
.map(({ name, icon }) => (
|
||||
<PanelSectionRow key={name}>
|
||||
<ButtonItem layout="below" onClick={() => setActivePlugin(name)}>
|
||||
@@ -59,6 +64,12 @@ const PluginView: VFC = () => {
|
||||
</ButtonItem>
|
||||
</PanelSectionRow>
|
||||
))}
|
||||
{hiddenPlugins.length > 0 && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem', marginTop: '10px' }}>
|
||||
<FaEyeSlash />
|
||||
<div>{t('PluginView.hidden', { count: hiddenPlugins.length })}</div>
|
||||
</div>
|
||||
)}
|
||||
</PanelSection>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -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<PluginUninstallModalProps> = ({ name, title, buttonText, description, closeModal }) => {
|
||||
return (
|
||||
<ConfirmModal
|
||||
closeModal={closeModal}
|
||||
onOK={async () => {
|
||||
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}
|
||||
</ConfirmModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginUninstallModal;
|
||||
@@ -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<PluginListLabelProps> = ({ name, hidden, version }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
||||
<div>{version ? `${name} - ${version}` : name}</div>
|
||||
{hidden && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: '0.8rem',
|
||||
color: '#dcdedf',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
}}
|
||||
>
|
||||
<FaEyeSlash />
|
||||
{t('PluginListLabel.hidden')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginListLabel;
|
||||
@@ -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<PluginData> }) {
|
||||
const data = props.entry.data;
|
||||
type PluginTableData = PluginData & { name: string; hidden: boolean; onHide(): void; onShow(): void };
|
||||
|
||||
function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }) {
|
||||
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<PluginData> }) {
|
||||
<MenuItem
|
||||
onSelected={() => {
|
||||
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<PluginData> }) {
|
||||
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<PluginData> }) {
|
||||
<MenuItem
|
||||
onSelected={() =>
|
||||
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')}
|
||||
</MenuItem>
|
||||
{hidden ? (
|
||||
<MenuItem onSelected={onShow}>{t('PluginListIndex.show')}</MenuItem>
|
||||
) : (
|
||||
<MenuItem onSelected={onHide}>{t('PluginListIndex.hide')}</MenuItem>
|
||||
)}
|
||||
</Menu>,
|
||||
e.currentTarget ?? window,
|
||||
);
|
||||
@@ -82,22 +91,22 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginData> }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{data?.update ? (
|
||||
{update ? (
|
||||
<DialogButton
|
||||
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
|
||||
onClick={() => 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)}
|
||||
>
|
||||
<div style={{ display: 'flex', minWidth: '180px', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
{t('PluginListIndex.update_to', { name: data?.update?.name })}
|
||||
{t('PluginListIndex.update_to', { name: update.name })}
|
||||
<FaDownload style={{ paddingLeft: '1rem' }} />
|
||||
</div>
|
||||
</DialogButton>
|
||||
) : (
|
||||
<DialogButton
|
||||
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
|
||||
onClick={() => reinstallPlugin(pluginName, data?.version)}
|
||||
onOKButton={() => reinstallPlugin(pluginName, data?.version)}
|
||||
onClick={() => reinstallPlugin(name, version)}
|
||||
onOKButton={() => reinstallPlugin(name, version)}
|
||||
>
|
||||
<div style={{ display: 'flex', minWidth: '180px', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
{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<string[]>(
|
||||
'pluginOrder',
|
||||
plugins.map((plugin) => plugin.name),
|
||||
@@ -141,22 +150,29 @@ export default function PluginList() {
|
||||
window.DeckyPluginLoader.checkPluginUpdates();
|
||||
}, []);
|
||||
|
||||
const [pluginEntries, setPluginEntries] = useState<ReorderableEntry<PluginData>[]>([]);
|
||||
const [pluginEntries, setPluginEntries] = useState<ReorderableEntry<PluginTableData>[]>([]);
|
||||
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: <PluginListLabel name={name} hidden={hidden} version={version} />,
|
||||
position: pluginOrder.indexOf(name),
|
||||
data: {
|
||||
update: updates?.get(plugin.name),
|
||||
version: plugin.version,
|
||||
name,
|
||||
hidden,
|
||||
version,
|
||||
update: updates?.get(name),
|
||||
onHide: () => hiddenPluginsService.update([...hiddenPlugins, name]),
|
||||
onShow: () => hiddenPluginsService.update(hiddenPlugins.filter((pluginName) => name !== pluginName)),
|
||||
},
|
||||
position: pluginOrder.indexOf(plugin.name),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}, [plugins, updates]);
|
||||
}, [plugins, updates, hiddenPlugins]);
|
||||
|
||||
if (plugins.length === 0) {
|
||||
return (
|
||||
@@ -166,8 +182,8 @@ export default function PluginList() {
|
||||
);
|
||||
}
|
||||
|
||||
function onSave(entries: ReorderableEntry<PluginData>[]) {
|
||||
const newOrder = entries.map((entry) => labelToName(entry.label, entry?.data?.version));
|
||||
function onSave(entries: ReorderableEntry<PluginTableData>[]) {
|
||||
const newOrder = entries.map((entry) => entry.data!.name);
|
||||
console.log(newOrder);
|
||||
setPluginOrder(newOrder);
|
||||
setPluginOrderSetting(newOrder);
|
||||
@@ -200,7 +216,7 @@ export default function PluginList() {
|
||||
</DialogButton>
|
||||
)}
|
||||
<DialogControlsSection style={{ marginTop: 0 }}>
|
||||
<ReorderableList<PluginData> entries={pluginEntries} onSave={onSave} interactables={PluginInteractables} />
|
||||
<ReorderableList<PluginTableData> entries={pluginEntries} onSave={onSave} interactables={PluginInteractables} />
|
||||
</DialogControlsSection>
|
||||
</DialogBody>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { DeckyState } from './components/DeckyState';
|
||||
import { getSetting, setSetting } from './utils/settings';
|
||||
|
||||
/**
|
||||
* A Service class for managing the state and actions related to the hidden plugins feature
|
||||
*
|
||||
* It's mostly responsible for sending setting updates to the server and keeping the local state in sync.
|
||||
*/
|
||||
export class HiddenPluginsService {
|
||||
constructor(private deckyState: DeckyState) {}
|
||||
|
||||
init() {
|
||||
getSetting<string[]>('hiddenPlugins', []).then((hiddenPlugins) => {
|
||||
this.deckyState.setHiddenPlugins(hiddenPlugins);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the new hidden plugins list to the server and persists it locally in the decky state
|
||||
*
|
||||
* @param hiddenPlugins The new list of hidden plugins
|
||||
*/
|
||||
async update(hiddenPlugins: string[]) {
|
||||
await setSetting('hiddenPlugins', hiddenPlugins);
|
||||
this.deckyState.setHiddenPlugins(hiddenPlugins);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the state of hidden plugins in the local state
|
||||
*/
|
||||
async invalidate() {
|
||||
this.deckyState.setHiddenPlugins(await getSetting('hiddenPlugins', []));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
ConfirmModal,
|
||||
ModalRoot,
|
||||
PanelSection,
|
||||
PanelSectionRow,
|
||||
@@ -18,9 +17,11 @@ import LegacyPlugin from './components/LegacyPlugin';
|
||||
import { deinitFilepickerPatches, initFilepickerPatches } from './components/modals/filepicker/patches';
|
||||
import MultiplePluginsInstallModal from './components/modals/MultiplePluginsInstallModal';
|
||||
import PluginInstallModal from './components/modals/PluginInstallModal';
|
||||
import PluginUninstallModal from './components/modals/PluginUninstallModal';
|
||||
import NotificationBadge from './components/NotificationBadge';
|
||||
import PluginView from './components/PluginView';
|
||||
import WithSuspense from './components/WithSuspense';
|
||||
import { HiddenPluginsService } from './hidden-plugins-service';
|
||||
import Logger from './logger';
|
||||
import { InstallType, Plugin } from './plugin';
|
||||
import RouterHook from './router-hook';
|
||||
@@ -45,6 +46,7 @@ class PluginLoader extends Logger {
|
||||
private routerHook: RouterHook = new RouterHook();
|
||||
public toaster: Toaster = new Toaster();
|
||||
private deckyState: DeckyState = new DeckyState();
|
||||
public hiddenPluginsService = new HiddenPluginsService(this.deckyState);
|
||||
|
||||
private reloadLock: boolean = false;
|
||||
// stores a list of plugin names which requested to be reloaded
|
||||
@@ -182,21 +184,8 @@ class PluginLoader extends Logger {
|
||||
);
|
||||
}
|
||||
|
||||
public uninstallPlugin(name: string, title: string, button_text: string, description: string) {
|
||||
showModal(
|
||||
<ConfirmModal
|
||||
onOK={async () => {
|
||||
await this.callServerMethod('uninstall_plugin', { name });
|
||||
}}
|
||||
onCancel={() => {
|
||||
// do nothing
|
||||
}}
|
||||
strTitle={title}
|
||||
strOKButtonText={button_text}
|
||||
>
|
||||
{description}
|
||||
</ConfirmModal>,
|
||||
);
|
||||
public uninstallPlugin(name: string, title: string, buttonText: string, description: string) {
|
||||
showModal(<PluginUninstallModal name={name} title={title} buttonText={buttonText} description={description} />);
|
||||
}
|
||||
|
||||
public hasPlugin(name: string) {
|
||||
@@ -220,6 +209,8 @@ class PluginLoader extends Logger {
|
||||
console.log(pluginOrder);
|
||||
this.deckyState.setPluginOrder(pluginOrder);
|
||||
});
|
||||
|
||||
this.hiddenPluginsService.init();
|
||||
}
|
||||
|
||||
public deinit() {
|
||||
|
||||
Reference in New Issue
Block a user