diff options
| author | Marco Rodolfi <marco.rodolfi@tuta.io> | 2026-10-18 02:12:24 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-10-17 20:12:24 -0400 |
| commit | 82df584b461fe9a283b687ad9d883ea1cb8767a8 (patch) | |
| tree | ffd1d13d6d133402300d3615b70a30538c8c7ced /frontend | |
| parent | 3c088117f0266852ec5ea780ff6f737dcfa9eab2 (diff) | |
| download | decky-loader-82df584b461fe9a283b687ad9d883ea1cb8767a8.tar.gz decky-loader-82df584b461fe9a283b687ad9d883ea1cb8767a8.zip | |
[Rebase] Warn users on possible risks on installing third party unvetted plugins (#676)
Co-authored-by: Marco Rodolfi <marco.rodolfi.1992@gmail.com>
Diffstat (limited to 'frontend')
| -rw-r--r-- | frontend/src/components/modals/WarnThirdParty.tsx | 78 | ||||
| -rw-r--r-- | frontend/src/components/settings/pages/developer/index.tsx | 29 | ||||
| -rw-r--r-- | frontend/src/components/settings/pages/general/StoreSelect.tsx | 44 | ||||
| -rw-r--r-- | frontend/src/utils/TranslationHelper.tsx | 29 | ||||
| -rw-r--r-- | frontend/src/utils/globalTypes.ts | 4 |
5 files changed, 161 insertions, 23 deletions
diff --git a/frontend/src/components/modals/WarnThirdParty.tsx b/frontend/src/components/modals/WarnThirdParty.tsx new file mode 100644 index 00000000..e7ff4561 --- /dev/null +++ b/frontend/src/components/modals/WarnThirdParty.tsx @@ -0,0 +1,78 @@ +import { ConfirmModal } from '@decky/ui'; +import { FC, useEffect, useState } from 'react'; +import { FaExclamationTriangle } from 'react-icons/fa'; + +import { WarnThirdPartyType } from '../../utils/globalTypes'; +import TranslationHelper, { TranslationClass } from '../../utils/TranslationHelper'; + +interface WarnThirdPartyProps { + seconds?: number; + type: WarnThirdPartyType; + onOK(): void; + onCancel(): void; + closeModal?(): void; +} + +const WarnThirdParty: FC<WarnThirdPartyProps> = ({ seconds = 5, type, onOK, onCancel, closeModal }) => { + const [waitTimer, setWaitTimer] = useState(seconds); + + useEffect(() => { + // exit early when we reach 0 + if (waitTimer <= 0) return; + + // save intervalId to clear the interval when the + // component re-renders + const intervalId = setInterval(() => { + setWaitTimer(waitTimer - 1); + }, 1000); + + // clear interval on re-render to avoid memory leaks + return () => clearInterval(intervalId); + // add waitTimer as a dependency to re-rerun the effect + // when we update it + }, [waitTimer]); + + return ( + <ConfirmModal + bOKDisabled={waitTimer > 0} + closeModal={closeModal} + onOK={async () => { + await onOK(); + }} + onCancel={async () => { + await onCancel(); + }} + strTitle={ + <div> + <FaExclamationTriangle /> + <TranslationHelper transClass={TranslationClass.WARN_THIRD_PARTY} transText="title" warnType={type} /> + </div> + } + strOKButtonText={ + waitTimer > 0 ? ( + <div> + <TranslationHelper + transClass={TranslationClass.WARN_THIRD_PARTY} + transText="button_processing" + i18nArgs={{ + timer: waitTimer, + }} + /> + </div> + ) : ( + <div> + <TranslationHelper transClass={TranslationClass.WARN_THIRD_PARTY} transText="button_idle" /> + </div> + ) + } + > + <span style={{ color: 'red' }}> + <div> + <TranslationHelper transClass={TranslationClass.WARN_THIRD_PARTY} transText="desc" warnType={type} /> + </div> + </span> + </ConfirmModal> + ); +}; + +export default WarnThirdParty; diff --git a/frontend/src/components/settings/pages/developer/index.tsx b/frontend/src/components/settings/pages/developer/index.tsx index 05989806..8691ff7b 100644 --- a/frontend/src/components/settings/pages/developer/index.tsx +++ b/frontend/src/components/settings/pages/developer/index.tsx @@ -7,6 +7,7 @@ import { Navigation, TextField, Toggle, + showModal, } from '@decky/ui'; import { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -15,9 +16,11 @@ import { FaFileArchive, FaLink, FaReact, FaSteamSymbol, FaTerminal } from 'react import { setShouldConnectToReactDevTools, setShowValveInternal } from '../../../../developer'; import Logger from '../../../../logger'; import { installFromURL } from '../../../../store'; +import { WarnThirdPartyType } from '../../../../utils/globalTypes'; import { useSetting } from '../../../../utils/hooks/useSetting'; import { getSetting } from '../../../../utils/settings'; import { FileSelectionType } from '../../../modals/filepicker'; +import WarnThirdParty from '../../../modals/WarnThirdParty'; import RemoteDebuggingSettings from '../general/RemoteDebugging'; const logger = new Logger('DeveloperIndex'); @@ -43,6 +46,8 @@ export default function DeveloperSettings() { const [enableValveInternal, setEnableValveInternal] = useSetting<boolean>('developer.valve_internal', false); const [reactDevtoolsEnabled, setReactDevtoolsEnabled] = useSetting<boolean>('developer.rdt.enabled', false); const [reactDevtoolsIP, setReactDevtoolsIP] = useSetting<string>('developer.rdt.ip', ''); + const [acceptedWarning, setAcceptedWarning] = useSetting<boolean>('developer.warn.third_party', false); + const waitTime = acceptedWarning ? 0 : 5; const [pluginURL, setPluginURL] = useState(''); const textRef = useRef<HTMLDivElement>(null); const { t } = useTranslation(); @@ -74,13 +79,23 @@ export default function DeveloperSettings() { > <DialogButton disabled={pluginURL.length == 0} - onClick={() => { - if (/^https?:\/\//.test(pluginURL)) { - installFromURL(pluginURL); - } else { - installFromURL('https://' + pluginURL); - } - }} + onClick={() => + showModal( + <WarnThirdParty + type={WarnThirdPartyType.ZIP} + onOK={() => { + setAcceptedWarning(true); + if (/^https?:\/\//.test(pluginURL)) { + installFromURL(pluginURL); + } else { + installFromURL('https://' + pluginURL); + } + }} + onCancel={() => {}} + seconds={waitTime} + />, + ) + } > {t('SettingsDeveloperIndex.third_party_plugins.button_install')} </DialogButton> diff --git a/frontend/src/components/settings/pages/general/StoreSelect.tsx b/frontend/src/components/settings/pages/general/StoreSelect.tsx index 9cc7d5c9..80bc4930 100644 --- a/frontend/src/components/settings/pages/general/StoreSelect.tsx +++ b/frontend/src/components/settings/pages/general/StoreSelect.tsx @@ -1,17 +1,21 @@ -import { Dropdown, Field, TextField } from '@decky/ui'; +import { Dropdown, Field, TextField, showModal } from '@decky/ui'; import { FunctionComponent } from 'react'; import { useTranslation } from 'react-i18next'; import { FaShapes } from 'react-icons/fa'; import Logger from '../../../../logger'; import { Store } from '../../../../store'; +import { WarnThirdPartyType } from '../../../../utils/globalTypes'; import { useSetting } from '../../../../utils/hooks/useSetting'; +import WarnThirdParty from '../../../modals/WarnThirdParty'; const logger = new Logger('StoreSelect'); const StoreSelect: FunctionComponent<{}> = () => { const [selectedStore, setSelectedStore] = useSetting<Store>('store', Store.Default); const [selectedStoreURL, setSelectedStoreURL] = useSetting<string | null>('store-url', null); + const [acceptedWarning, setAcceptedWarning] = useSetting<boolean>('store_select.warn.third_party', false); + const waitTime = acceptedWarning ? 0 : 5; const { t } = useTranslation(); const tStores = [ t('StoreSelect.store_channel.default'), @@ -38,20 +42,30 @@ const StoreSelect: FunctionComponent<{}> = () => { }} /> </Field> - {selectedStore == Store.Custom && ( - <Field - label={t('StoreSelect.custom_store.label')} - indentLevel={1} - description={ - <TextField - label={t('StoreSelect.custom_store.url_label')} - value={selectedStoreURL || undefined} - onChange={(e) => setSelectedStoreURL(e?.target.value || null)} - /> - } - icon={<FaShapes style={{ display: 'block' }} />} - ></Field> - )} + {selectedStore == Store.Custom && + showModal( + <WarnThirdParty + type={WarnThirdPartyType.REPO} + seconds={waitTime} + onOK={() => { + setAcceptedWarning(true); + }} + onCancel={() => setSelectedStore(Store.Default)} + />, + ) && ( + <Field + label={t('StoreSelect.custom_store.label')} + indentLevel={1} + description={ + <TextField + label={t('StoreSelect.custom_store.url_label')} + value={selectedStoreURL || undefined} + onChange={(e) => setSelectedStoreURL(e?.target.value || null)} + /> + } + icon={<FaShapes style={{ display: 'block' }} />} + ></Field> + )} </> ); }; diff --git a/frontend/src/utils/TranslationHelper.tsx b/frontend/src/utils/TranslationHelper.tsx index 4596060d..60af2065 100644 --- a/frontend/src/utils/TranslationHelper.tsx +++ b/frontend/src/utils/TranslationHelper.tsx @@ -2,10 +2,12 @@ import { FC } from 'react'; import { Translation } from 'react-i18next'; import Logger from '../logger'; +import { WarnThirdPartyType } from './globalTypes'; export enum TranslationClass { PLUGIN_LOADER = 'PluginLoader', DEVELOPER = 'Developer', + WARN_THIRD_PARTY = 'WarnThirdParty', } interface TranslationHelperProps { @@ -13,11 +15,17 @@ interface TranslationHelperProps { transText: string; i18nArgs?: {}; installType?: number; + warnType?: WarnThirdPartyType; } const logger = new Logger('TranslationHelper'); -const TranslationHelper: FC<TranslationHelperProps> = ({ transClass, transText, i18nArgs = null }) => { +const TranslationHelper: FC<TranslationHelperProps> = ({ + transClass, + transText, + i18nArgs = null, + warnType = WarnThirdPartyType.REPO, +}) => { return ( <Translation> {(t, {}) => { @@ -30,6 +38,25 @@ const TranslationHelper: FC<TranslationHelperProps> = ({ transClass, transText, return i18nArgs ? t(TranslationClass.DEVELOPER + '.' + transText, i18nArgs) : t(TranslationClass.DEVELOPER + '.' + transText); + //Handle different messages in different class cases + case TranslationClass.WARN_THIRD_PARTY: + //Needed only for title and description + if (!transText.startsWith('button')) { + switch (warnType) { + case WarnThirdPartyType.REPO: + return i18nArgs + ? t(TranslationClass.WARN_THIRD_PARTY + '.' + transText + '_repo', i18nArgs) + : t(TranslationClass.WARN_THIRD_PARTY + '.' + transText + '_repo'); + case WarnThirdPartyType.ZIP: + return i18nArgs + ? t(TranslationClass.WARN_THIRD_PARTY + '.' + transText + '_zip', i18nArgs) + : t(TranslationClass.WARN_THIRD_PARTY + '.' + transText + '_zip'); + } + } else { + return i18nArgs + ? t(TranslationClass.WARN_THIRD_PARTY + '.' + transText, i18nArgs) + : t(TranslationClass.WARN_THIRD_PARTY + '.' + transText); + } default: logger.error('We should never fall in the default case!'); return ''; diff --git a/frontend/src/utils/globalTypes.ts b/frontend/src/utils/globalTypes.ts new file mode 100644 index 00000000..26f7d1ae --- /dev/null +++ b/frontend/src/utils/globalTypes.ts @@ -0,0 +1,4 @@ +export enum WarnThirdPartyType { + REPO = 0, + ZIP = 1, +} |
