diff --git a/backend/decky_loader/locales/en-US.json b/backend/decky_loader/locales/en-US.json index 23566026b..b82686e1b 100644 --- a/backend/decky_loader/locales/en-US.json +++ b/backend/decky_loader/locales/en-US.json @@ -254,6 +254,15 @@ "testing": "Testing" } }, + "WarnThirdParty":{ + "title_zip": "Third-Party Plugin Installation", + "title_repo": "Third-Party Store Selection", + "button_processing_one": "Please wait {{timer}} second", + "button_processing_many": "Please wait {{timer}} seconds", + "button_idle": "Continue", + "desc_zip": "The Decky Loader team has not reviewed this plugin. It may contain malware, such as software to steal your Steam account or harm your device. By installing this plugin, you agree you have assumed all risks to your device.", + "desc_repo": "The Decky Loader team does not maintain this plugin store. It and its plugins may contain malware, such as software to steal your Steam account or harm your device. By adding this store, you agree you have assumed all risks to your device." + }, "Testing": { "download": "Download", "error": "Error Installing PR", diff --git a/frontend/src/components/modals/WarnThirdParty.tsx b/frontend/src/components/modals/WarnThirdParty.tsx new file mode 100644 index 000000000..e7ff45617 --- /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 = ({ 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 ( + 0} + closeModal={closeModal} + onOK={async () => { + await onOK(); + }} + onCancel={async () => { + await onCancel(); + }} + strTitle={ +
+ + +
+ } + strOKButtonText={ + waitTimer > 0 ? ( +
+ +
+ ) : ( +
+ +
+ ) + } + > + +
+ +
+
+
+ ); +}; + +export default WarnThirdParty; diff --git a/frontend/src/components/settings/pages/developer/index.tsx b/frontend/src/components/settings/pages/developer/index.tsx index 099f26101..e5f355468 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('developer.valve_internal', false); const [reactDevtoolsEnabled, setReactDevtoolsEnabled] = useSetting('developer.rdt.enabled', false); const [reactDevtoolsIP, setReactDevtoolsIP] = useSetting('developer.rdt.ip', ''); + const [acceptedWarning, setAcceptedWarning] = useSetting('developer.warn.third_party', false); + const waitTime = acceptedWarning ? 0 : 5; const [pluginURL, setPluginURL] = useState(''); const textRef = useRef(null); const { t } = useTranslation(); @@ -72,7 +77,22 @@ export default function DeveloperSettings() { } icon={} > - installFromURL(pluginURL)}> + + showModal( + { + setAcceptedWarning(true); + installFromURL(pluginURL); + }} + onCancel={() => {}} + seconds={waitTime} + />, + ) + } + > {t('SettingsDeveloperIndex.third_party_plugins.button_install')} diff --git a/frontend/src/components/settings/pages/general/StoreSelect.tsx b/frontend/src/components/settings/pages/general/StoreSelect.tsx index 9cc7d5c9d..80bc4930c 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.Default); const [selectedStoreURL, setSelectedStoreURL] = useSetting('store-url', null); + const [acceptedWarning, setAcceptedWarning] = useSetting('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<{}> = () => { }} /> - {selectedStore == Store.Custom && ( - setSelectedStoreURL(e?.target.value || null)} - /> - } - icon={} - > - )} + {selectedStore == Store.Custom && + showModal( + { + setAcceptedWarning(true); + }} + onCancel={() => setSelectedStore(Store.Default)} + />, + ) && ( + setSelectedStoreURL(e?.target.value || null)} + /> + } + icon={} + > + )} ); }; diff --git a/frontend/src/utils/TranslationHelper.tsx b/frontend/src/utils/TranslationHelper.tsx index 61bd24bfb..c499f39da 100644 --- a/frontend/src/utils/TranslationHelper.tsx +++ b/frontend/src/utils/TranslationHelper.tsx @@ -3,11 +3,13 @@ import { Translation } from 'react-i18next'; import Logger from '../logger'; import { InstallType } from '../plugin'; +import { WarnThirdPartyType } from './globalTypes'; export enum TranslationClass { PLUGIN_LOADER = 'PluginLoader', PLUGIN_INSTALL_MODAL = 'PluginInstallModal', DEVELOPER = 'Developer', + WARN_THIRD_PARTY = 'WarnThirdParty', } interface TranslationHelperProps { @@ -15,11 +17,18 @@ interface TranslationHelperProps { transText: string; i18nArgs?: {}; installType?: number; + warnType?: WarnThirdPartyType; } const logger = new Logger('TranslationHelper'); -const TranslationHelper: FC = ({ transClass, transText, i18nArgs = null, installType = 0 }) => { +const TranslationHelper: FC = ({ + transClass, + transText, + i18nArgs = null, + installType = 0, + warnType = WarnThirdPartyType.REPO, +}) => { return ( {(t, {}) => { @@ -47,6 +56,25 @@ const TranslationHelper: FC = ({ 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 000000000..26f7d1aec --- /dev/null +++ b/frontend/src/utils/globalTypes.ts @@ -0,0 +1,4 @@ +export enum WarnThirdPartyType { + REPO = 0, + ZIP = 1, +}