Skip to content

feat: allow opening hidden plugins #736

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
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
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
}
},
"dependencies": {
"@decky/ui": "^4.8.3",
"@decky/ui": "^4.9.0",
"compare-versions": "^6.1.1",
"filesize": "^10.1.2",
"i18next": "^23.11.5",
Expand Down
10 changes: 5 additions & 5 deletions frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion frontend/src/components/PluginView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ const PluginView: FC = () => {

if (activePlugin) {
return (
<Focusable onCancelButton={closeActivePlugin}>
<Focusable
onCancelButton={closeActivePlugin}
// Needed to focus inside the panel when opening plugin from settings menu
// Doesn't seem to do any harm (since this seems to need focus in normal cases too) so I made this unconditional
autoFocus
>
<TitleView />
<div style={{ height: '100%', paddingTop: '16px' }}>
<ErrorBoundary>{(visible || activePlugin.alwaysRender) && activePlugin.content}</ErrorBoundary>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/TitleView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const TitleView: FC = () => {
>
<FaArrowLeft style={{ marginTop: '-4px', display: 'block' }} />
</DialogButton>
{activePlugin?.titleView || <div style={{ flex: 0.9 }}>{activePlugin.name}</div>}
{activePlugin.titleView || <div style={{ flex: 0.9 }}>{activePlugin.name}</div>}
</Focusable>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,27 @@ import { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { FaEyeSlash, FaLock } from 'react-icons/fa';

import { StorePluginVersion } from '../../../../store';
import NotificationBadge from '../../../NotificationBadge';

interface PluginListLabelProps {
frozen: boolean;
hidden: boolean;
name: string;
version?: string;
update: StorePluginVersion | undefined;
}

const PluginListLabel: FC<PluginListLabelProps> = ({ name, frozen, hidden, version }) => {
const PluginListLabel: FC<PluginListLabelProps> = ({ name, frozen, hidden, version, update }) => {
const { t } = useTranslation();
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
<div>
<div
style={{
// needed for NotificationBadge
position: 'relative',
}}
>
{name}
{version && (
<>
Expand All @@ -28,6 +37,7 @@ const PluginListLabel: FC<PluginListLabelProps> = ({ name, frozen, hidden, versi
</span>
</>
)}
<NotificationBadge show={!!update} style={{ top: '-5px', right: '-10px' }} />
</div>
{hidden && (
<div
Expand Down
110 changes: 63 additions & 47 deletions frontend/src/components/settings/pages/plugin_list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import {
GamepadEvent,
Menu,
MenuItem,
MenuSeparator,
Navigation,
QuickAccessTab,
ReorderableEntry,
ReorderableList,
showContextMenu,
} from '@decky/ui';
import { useEffect, useState } from 'react';
import { CSSProperties, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { FaDownload, FaEllipsisH, FaRecycle } from 'react-icons/fa';
import { FaDownload, FaEllipsisH } from 'react-icons/fa';
import { TbLayoutSidebarRightExpandFilled } from 'react-icons/tb';

import { InstallType } from '../../../../plugin';
import {
Expand Down Expand Up @@ -41,11 +45,23 @@ type PluginTableData = PluginData & {
hidden: boolean;
onHide(): void;
onShow(): void;
onOpen(): void;
isDeveloper: boolean;
};

const reloadPluginBackend = DeckyBackend.callable<[pluginName: string], void>('loader/reload_plugin');

const squareButtonStyle: CSSProperties = {
height: '40px',
width: '40px',
minWidth: '40px',
padding: '0',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
};

function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }) {
const { t } = useTranslation();

Expand All @@ -54,7 +70,8 @@ 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, onOpen } =
props.entry.data;

const showCtxMenu = (e: MouseEvent | GamepadEvent) => {
showContextMenu(
Expand All @@ -70,6 +87,7 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
>
{t('PluginListIndex.reload')}
</MenuItem>
<MenuItem onSelected={() => reinstallPlugin(name, version)}>{t('PluginListIndex.reinstall')}</MenuItem>
<MenuItem
onSelected={() =>
DeckyPluginLoader.uninstallPlugin(
Expand All @@ -82,6 +100,9 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
>
{t('PluginListIndex.uninstall')}
</MenuItem>

<MenuSeparator />

{hidden ? (
<MenuItem onSelected={onShow}>{t('PluginListIndex.show')}</MenuItem>
) : (
Expand All @@ -98,46 +119,34 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> }
};

return (
<>
<div style={{ display: 'flex', gap: '10px' }}>
{update ? (
<DialogButton
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
style={{
flex: '1 1',
minWidth: 'unset',
height: '40px',
display: 'flex',
gap: '1rem',
justifyContent: 'center',
alignItems: 'center',
}}
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: update.name })}
<FaDownload style={{ paddingLeft: '1rem' }} />
</div>
{t('PluginListIndex.update_to', { name: update.name })} <FaDownload />
</DialogButton>
) : (
<DialogButton
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
onClick={() => reinstallPlugin(name, version)}
onOKButton={() => reinstallPlugin(name, version)}
>
<div style={{ display: 'flex', minWidth: '180px', justifyContent: 'space-between', alignItems: 'center' }}>
{t('PluginListIndex.reinstall')}
<FaRecycle style={{ paddingLeft: '1rem' }} />
</div>
</DialogButton>
)}
<DialogButton
style={{
height: '40px',
width: '40px',
padding: '10px 12px',
minWidth: '40px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
}}
onClick={showCtxMenu}
onOKButton={showCtxMenu}
>
) : null}
<DialogButton style={squareButtonStyle} onClick={onOpen} onOKButton={onOpen}>
<TbLayoutSidebarRightExpandFilled
// make size more consistent with the rest of icons in app
size={'1.5rem'}
/>
</DialogButton>
<DialogButton style={squareButtonStyle} onClick={showCtxMenu} onOKButton={showCtxMenu}>
<FaEllipsisH />
</DialogButton>
</>
</div>
);
}

Expand All @@ -147,7 +156,8 @@ type PluginData = {
};

export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) {
const { plugins, updates, pluginOrder, setPluginOrder, frozenPlugins, hiddenPlugins } = useDeckyState();
const { plugins, updates, pluginOrder, setPluginOrder, frozenPlugins, hiddenPlugins, setActivePlugin } =
useDeckyState();
const [_, setPluginOrderSetting] = useSetting<string[]>(
'pluginOrder',
plugins.map((plugin) => plugin.name),
Expand All @@ -158,35 +168,40 @@ export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) {
DeckyPluginLoader.checkPluginUpdates();
}, []);

const [pluginEntries, setPluginEntries] = useState<ReorderableEntry<PluginTableData>[]>([]);
const hiddenPluginsService = DeckyPluginLoader.hiddenPluginsService;
const frozenPluginsService = DeckyPluginLoader.frozenPluginsService;

useEffect(() => {
setPluginEntries(
const pluginEntries = useMemo(
() =>
plugins.map(({ name, version }) => {
const hiddenPluginsService = DeckyPluginLoader.hiddenPluginsService;
const frozenPluginsService = DeckyPluginLoader.frozenPluginsService;

const frozen = frozenPlugins.includes(name);
const hidden = hiddenPlugins.includes(name);

const update = updates?.get(name);

return {
label: <PluginListLabel name={name} frozen={frozen} hidden={hidden} version={version} />,
label: <PluginListLabel name={name} frozen={frozen} hidden={hidden} version={version} update={update} />,
position: pluginOrder.indexOf(name),
data: {
name,
frozen,
hidden,
isDeveloper,
version,
update: updates?.get(name),
update,
onFreeze: () => frozenPluginsService.update([...frozenPlugins, name]),
onUnfreeze: () => frozenPluginsService.update(frozenPlugins.filter((pluginName) => name !== pluginName)),
onHide: () => hiddenPluginsService.update([...hiddenPlugins, name]),
onShow: () => hiddenPluginsService.update(hiddenPlugins.filter((pluginName) => name !== pluginName)),
onOpen: () => {
setActivePlugin(name);
Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky);
},
},
};
}),
);
}, [plugins, updates, hiddenPlugins]);
[plugins, updates, frozenPlugins, hiddenPlugins, setActivePlugin],
);

if (plugins.length === 0) {
return (
Expand Down Expand Up @@ -223,10 +238,11 @@ export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) {
width: 'auto',
display: 'flex',
alignItems: 'center',
gap: '1rem',
}}
>
{t('PluginListIndex.update_all', { count: updates.size })}
<FaDownload style={{ paddingLeft: '1rem' }} />
<FaDownload />
</DialogButton>
)}
<DialogControlsSection style={{ marginTop: 0 }}>
Expand Down