diff options
Diffstat (limited to 'frontend/src/components')
11 files changed, 212 insertions, 51 deletions
diff --git a/frontend/src/components/DeckyState.tsx b/frontend/src/components/DeckyState.tsx index d2ac63ae..d1b558c1 100644 --- a/frontend/src/components/DeckyState.tsx +++ b/frontend/src/components/DeckyState.tsx @@ -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[]; + disabledPlugins: DisabledPlugin[]; + installedPlugins: (Plugin | DisabledPlugin)[]; pluginOrder: string[]; frozenPlugins: string[]; hiddenPlugins: string[]; @@ -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[] = []; @@ -42,6 +46,8 @@ export class DeckyState { publicState(): PublicDeckyState { return { plugins: this._plugins, + disabledPlugins: this._disabledPlugins, + installedPlugins: this._installedPlugins, pluginOrder: this._pluginOrder, frozenPlugins: this._frozenPlugins, hiddenPlugins: this._hiddenPlugins, @@ -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(); } @@ -125,6 +138,7 @@ interface DeckyStateContext extends PublicDeckyState { setIsLoaderUpdating(hasUpdate: boolean): void; setActivePlugin(name: string): void; setPluginOrder(pluginOrder: string[]): void; + setDisabledPlugins(disabled: DisabledPlugin[]): void; closeActivePlugin(): void; } @@ -163,6 +177,7 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) = const setActivePlugin = deckyState.setActivePlugin.bind(deckyState); const closeActivePlugin = deckyState.closeActivePlugin.bind(deckyState); const setPluginOrder = deckyState.setPluginOrder.bind(deckyState); + const setDisabledPlugins = deckyState.setDisabledPlugins.bind(deckyState); return ( <DeckyStateContext.Provider @@ -173,6 +188,7 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) = setActivePlugin, closeActivePlugin, setPluginOrder, + setDisabledPlugins, }} > {children} diff --git a/frontend/src/components/PluginView.tsx b/frontend/src/components/PluginView.tsx index 1d39972e..ffaa176a 100644 --- a/frontend/src/components/PluginView.tsx +++ b/frontend/src/components/PluginView.tsx @@ -1,7 +1,7 @@ import { ButtonItem, ErrorBoundary, Focusable, PanelSection, PanelSectionRow } from '@decky/ui'; import { FC, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { FaEyeSlash } from 'react-icons/fa'; +import { FaBan, FaEyeSlash } from 'react-icons/fa'; import { useDeckyState } from './DeckyState'; import NotificationBadge from './NotificationBadge'; @@ -9,8 +9,16 @@ import { useQuickAccessVisible } from './QuickAccessVisibleState'; import TitleView from './TitleView'; const PluginView: FC = () => { - const { plugins, hiddenPlugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = - useDeckyState(); + const { + plugins, + disabledPlugins, + hiddenPlugins, + updates, + activePlugin, + pluginOrder, + setActivePlugin, + closeActivePlugin, + } = useDeckyState(); const visible = useQuickAccessVisible(); const { t } = useTranslation(); @@ -21,7 +29,9 @@ const PluginView: FC = () => { .sort((a, b) => pluginOrder.indexOf(a.name) - pluginOrder.indexOf(b.name)) .filter((p) => p.content) .filter(({ name }) => !hiddenPlugins.includes(name)); - }, [plugins, pluginOrder]); + }, [plugins, pluginOrder, hiddenPlugins]); + + const numberOfHidden = hiddenPlugins.filter((name) => !!plugins.find((p) => p.name === name)).length; if (activePlugin) { return ( @@ -53,12 +63,28 @@ const PluginView: FC = () => { </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> - )} + <div + style={{ + display: 'flex', + flexDirection: 'column', + position: 'absolute', + justifyContent: 'center', + padding: '5px 0px', + }} + > + {numberOfHidden > 0 && ( + <div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem' }}> + <FaEyeSlash /> + <div>{t('PluginView.hidden', { count: numberOfHidden })}</div> + </div> + )} + {disabledPlugins.length > 0 && ( + <div style={{ display: 'flex', alignItems: 'center', gap: '10px', fontSize: '0.8rem' }}> + <FaBan /> + <div>{t('PluginView.disabled', { count: disabledPlugins.length })}</div> + </div> + )} + </div> </PanelSection> </div> </> diff --git a/frontend/src/components/modals/MultiplePluginsInstallModal.tsx b/frontend/src/components/modals/MultiplePluginsInstallModal.tsx index 9c86f3db..e5c1c647 100644 --- a/frontend/src/components/modals/MultiplePluginsInstallModal.tsx +++ b/frontend/src/components/modals/MultiplePluginsInstallModal.tsx @@ -3,10 +3,11 @@ import { FC, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { FaCheck, FaDownload } from 'react-icons/fa'; -import { InstallType, InstallTypeTranslationMapping } from '../../plugin'; +import { DisabledPlugin, InstallType, InstallTypeTranslationMapping } from '../../plugin'; interface MultiplePluginsInstallModalProps { requests: { name: string; version: string; hash: string; install_type: InstallType }[]; + disabledPlugins: DisabledPlugin[]; onOK(): void | Promise<void>; onCancel(): void | Promise<void>; closeModal?(): void; @@ -17,6 +18,7 @@ type TitleTranslationMapping = 'mixed' | (typeof InstallTypeTranslationMapping)[ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({ requests, + disabledPlugins, onOK, onCancel, closeModal, @@ -116,10 +118,11 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({ version, }); + const disabled = disabledPlugins.some((p) => p.name === name); return ( <li key={i} style={{ display: 'flex', flexDirection: 'column' }}> <span> - {description}{' '} + {disabled ? `${description} - ${t('PluginInstallModal.disabled')}` : description}{' '} {(pluginsCompleted.includes(name) && <FaCheck />) || (name === pluginInProgress && <FaDownload />)} </span> {hash === 'False' && ( diff --git a/frontend/src/components/modals/PluginDisableModal.tsx b/frontend/src/components/modals/PluginDisableModal.tsx new file mode 100644 index 00000000..16ddd4bf --- /dev/null +++ b/frontend/src/components/modals/PluginDisableModal.tsx @@ -0,0 +1,39 @@ +import { ConfirmModal, Spinner } from '@decky/ui'; +import { FC, useState } from 'react'; + +import { disablePlugin } from '../../plugin'; + +interface PluginDisableModalProps { + name: string; + title: string; + buttonText: string; + description: string; + closeModal?(): void; +} + +const PluginDisableModal: FC<PluginDisableModalProps> = ({ name, title, buttonText, description, closeModal }) => { + const [disabling, setDisabling] = useState<boolean>(false); + return ( + <ConfirmModal + closeModal={closeModal} + onOK={async () => { + setDisabling(true); + await disablePlugin(name); + 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; diff --git a/frontend/src/components/modals/PluginInstallModal.tsx b/frontend/src/components/modals/PluginInstallModal.tsx index 16419d91..0075fce5 100644 --- a/frontend/src/components/modals/PluginInstallModal.tsx +++ b/frontend/src/components/modals/PluginInstallModal.tsx @@ -9,6 +9,7 @@ interface PluginInstallModalProps { version: string; hash: string; installType: InstallType; + disabled?: boolean; onOK(): void; onCancel(): void; closeModal?(): void; @@ -19,6 +20,7 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ version, hash, installType, + disabled, onOK, onCancel, closeModal, @@ -45,6 +47,10 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ }, []); const installTypeTranslationKey = InstallTypeTranslationMapping[installType]; + const description = t(`PluginInstallModal.${installTypeTranslationKey}.desc`, { + artifact: artifact, + version: version, + }); return ( <ConfirmModal @@ -118,10 +124,7 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({ // t('PluginInstallModal.update.desc') // t('PluginInstallModal.downgrade.desc') // t('PluginInstallModal.overwrite.desc') - t(`PluginInstallModal.${installTypeTranslationKey}.desc`, { - artifact: artifact, - version: version, - }) + disabled ? `${description} ${t('PluginInstallModal.disabled')}` : description } </div> {hash == 'False' && <span style={{ color: 'red' }}>{t('PluginInstallModal.no_hash')}</span>} diff --git a/frontend/src/components/modals/PluginUninstallModal.tsx b/frontend/src/components/modals/PluginUninstallModal.tsx index be479859..37d3d789 100644 --- a/frontend/src/components/modals/PluginUninstallModal.tsx +++ b/frontend/src/components/modals/PluginUninstallModal.tsx @@ -2,8 +2,10 @@ import { ConfirmModal, Spinner } from '@decky/ui'; import { FC, useState } from 'react'; import { uninstallPlugin } from '../../plugin'; +import { DeckyState } from '../DeckyState'; interface PluginUninstallModalProps { + deckyState: DeckyState; name: string; title: string; buttonText: string; @@ -11,7 +13,14 @@ interface PluginUninstallModalProps { closeModal?(): void; } -const PluginUninstallModal: FC<PluginUninstallModalProps> = ({ name, title, buttonText, description, closeModal }) => { +const PluginUninstallModal: FC<PluginUninstallModalProps> = ({ + name, + title, + buttonText, + description, + deckyState, + closeModal, +}) => { const [uninstalling, setUninstalling] = useState<boolean>(false); return ( <ConfirmModal @@ -19,6 +28,7 @@ const PluginUninstallModal: FC<PluginUninstallModalProps> = ({ name, title, butt onOK={async () => { setUninstalling(true); await uninstallPlugin(name); + deckyState.setDisabledPlugins(deckyState.publicState().disabledPlugins.filter((d) => d.name !== 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 DeckyPluginLoader.frozenPluginsService.invalidate(); diff --git a/frontend/src/components/settings/pages/plugin_list/PluginListLabel.tsx b/frontend/src/components/settings/pages/plugin_list/PluginListLabel.tsx index fec03e56..59171b39 100644 --- a/frontend/src/components/settings/pages/plugin_list/PluginListLabel.tsx +++ b/frontend/src/components/settings/pages/plugin_list/PluginListLabel.tsx @@ -1,15 +1,16 @@ import { FC } from 'react'; import { useTranslation } from 'react-i18next'; -import { FaEyeSlash, FaLock } from 'react-icons/fa'; +import { FaBan, FaEyeSlash, FaLock } 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' }}> @@ -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', + }} + > + <FaBan /> + {t('PluginListLabel.disabled')} + </div> + )} </div> ); }; diff --git a/frontend/src/components/settings/pages/plugin_list/index.tsx b/frontend/src/components/settings/pages/plugin_list/index.tsx index 9a7cb076..43d79709 100644 --- a/frontend/src/components/settings/pages/plugin_list/index.tsx +++ b/frontend/src/components/settings/pages/plugin_list/index.tsx @@ -2,9 +2,11 @@ import { DialogBody, DialogButton, DialogControlsSection, + Focusable, GamepadEvent, Menu, MenuItem, + NavEntryPositionPreferences, ReorderableEntry, ReorderableList, showContextMenu, @@ -13,7 +15,7 @@ import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { FaDownload, FaEllipsisH, FaRecycle } from 'react-icons/fa'; -import { InstallType } from '../../../../plugin'; +import { InstallType, enablePlugin } from '../../../../plugin'; import { StorePluginVersion, getPluginList, @@ -35,6 +37,7 @@ async function reinstallPlugin(pluginName: string, currentVersion?: string) { type PluginTableData = PluginData & { name: string; + disabled: boolean; frozen: boolean; onFreeze(): void; onUnfreeze(): void; @@ -54,22 +57,25 @@ 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( <Menu label={t('PluginListIndex.plugin_actions')}> - <MenuItem - onSelected={async () => { - try { - await reloadPluginBackend(name); - } catch (err) { - console.error('Error Reloading Plugin Backend', err); - } - }} - > - {t('PluginListIndex.reload')} - </MenuItem> + {!disabled && ( + <MenuItem + onSelected={async () => { + try { + await reloadPluginBackend(name); + } catch (err) { + console.error('Error Reloading Plugin Backend', err); + } + }} + > + {t('PluginListIndex.reload')} + </MenuItem> + )} <MenuItem onSelected={() => DeckyPluginLoader.uninstallPlugin( @@ -82,11 +88,28 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> } > {t('PluginListIndex.uninstall')} </MenuItem> - {hidden ? ( - <MenuItem onSelected={onShow}>{t('PluginListIndex.show')}</MenuItem> + {disabled ? ( + <MenuItem onSelected={() => enablePlugin(name)}>{t('PluginListIndex.enable')}</MenuItem> ) : ( - <MenuItem onSelected={onHide}>{t('PluginListIndex.hide')}</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.disable')} + </MenuItem> )} + {!disabled && + (hidden ? ( + <MenuItem onSelected={onShow}>{t('PluginListIndex.show')}</MenuItem> + ) : ( + <MenuItem onSelected={onHide}>{t('PluginListIndex.hide')}</MenuItem> + ))} {frozen ? ( <MenuItem onSelected={onUnfreeze}>{t('PluginListIndex.unfreeze')}</MenuItem> ) : ( @@ -98,7 +121,7 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> } }; return ( - <> + <Focusable navEntryPreferPosition={NavEntryPositionPreferences.MAINTAIN_X} style={{ display: 'flex' }}> {update ? ( <DialogButton style={{ height: '40px', minWidth: '60px', marginRight: '10px' }} @@ -137,7 +160,7 @@ function PluginInteractables(props: { entry: ReorderableEntry<PluginTableData> } > <FaEllipsisH /> </DialogButton> - </> + </Focusable> ); } @@ -147,16 +170,18 @@ type PluginData = { }; export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) { - const { plugins, updates, pluginOrder, setPluginOrder, frozenPlugins, hiddenPlugins } = useDeckyState(); + const { installedPlugins, disabledPlugins, updates, pluginOrder, setPluginOrder, frozenPlugins, hiddenPlugins } = + useDeckyState(); + const [_, setPluginOrderSetting] = useSetting<string[]>( 'pluginOrder', - plugins.map((plugin) => plugin.name), + installedPlugins.map((plugin) => plugin.name), ); const { t } = useTranslation(); useEffect(() => { DeckyPluginLoader.checkPluginUpdates(); - }, []); + }, [installedPlugins, frozenPlugins]); const [pluginEntries, setPluginEntries] = useState<ReorderableEntry<PluginTableData>[]>([]); const hiddenPluginsService = DeckyPluginLoader.hiddenPluginsService; @@ -164,15 +189,24 @@ 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={disabledPlugins.find((p) => p.name == name) !== undefined} + /> + ), position: pluginOrder.indexOf(name), data: { name, + disabled: disabledPlugins.some((disabledPlugin) => disabledPlugin.name === name), frozen, hidden, isDeveloper, @@ -186,9 +220,9 @@ export default function PluginList({ isDeveloper }: { isDeveloper: boolean }) { }; }), ); - }, [plugins, updates, hiddenPlugins]); + }, [installedPlugins, updates, hiddenPlugins, disabledPlugins]); - if (plugins.length === 0) { + if (installedPlugins.length === 0) { return ( <div> <p>{t('PluginListIndex.no_plugin')}</p> diff --git a/frontend/src/components/settings/pages/testing/index.tsx b/frontend/src/components/settings/pages/testing/index.tsx index 8f02c207..3c032361 100644 --- a/frontend/src/components/settings/pages/testing/index.tsx +++ b/frontend/src/components/settings/pages/testing/index.tsx @@ -4,6 +4,7 @@ import { DialogControlsSection, Field, Focusable, + NavEntryPositionPreferences, Navigation, ProgressBar, SteamSpinner, @@ -87,7 +88,10 @@ export default function TestingVersionList() { </> } > - <Focusable style={{ height: '40px', marginLeft: 'auto', display: 'flex' }}> + <Focusable + style={{ height: '40px', marginLeft: 'auto', display: 'flex' }} + navEntryPreferPosition={NavEntryPositionPreferences.MAINTAIN_X} + > <DialogButton style={{ height: '40px', minWidth: '60px', marginRight: '10px' }} onClick={async () => { diff --git a/frontend/src/components/store/PluginCard.tsx b/frontend/src/components/store/PluginCard.tsx index f64abd09..a47207c9 100644 --- a/frontend/src/components/store/PluginCard.tsx +++ b/frontend/src/components/store/PluginCard.tsx @@ -1,15 +1,23 @@ -import { ButtonItem, Dropdown, Focusable, PanelSectionRow, SingleDropdownOption, SuspensefulImage } from '@decky/ui'; +import { + ButtonItem, + Dropdown, + Focusable, + NavEntryPositionPreferences, + PanelSectionRow, + SingleDropdownOption, + SuspensefulImage, +} from '@decky/ui'; 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 }) => { @@ -139,7 +147,10 @@ const PluginCard: FC<PluginCardProps> = ({ storePlugin, installedPlugin }) => { </div> <div className="deckyStoreCardButtonRow"> <PanelSectionRow> - <Focusable style={{ display: 'flex', gap: '5px', padding: 0 }}> + <Focusable + style={{ display: 'flex', gap: '5px', padding: 0 }} + navEntryPreferPosition={NavEntryPositionPreferences.MAINTAIN_X} + > <div className="deckyStoreCardInstallContainer" style={ diff --git a/frontend/src/components/store/Store.tsx b/frontend/src/components/store/Store.tsx index 3209ba08..72187cbc 100644 --- a/frontend/src/components/store/Store.tsx +++ b/frontend/src/components/store/Store.tsx @@ -105,7 +105,7 @@ const BrowseTab: FC<{ setPluginCount: Dispatch<SetStateAction<number | null>> }> })(); }, []); - const { plugins: installedPlugins } = useDeckyState(); + const { installedPlugins } = useDeckyState(); return ( <> |
