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..a6a4c3f66 --- /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 TranslationHelper, { TranslationClass } from '../../utils/TranslationHelper'; +import { WarnThirdPartyType } from '../../utils/globalTypes'; + +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..f828b39e2 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'; @@ -18,6 +19,8 @@ import { installFromURL } from '../../../../store'; import { useSetting } from '../../../../utils/hooks/useSetting'; import { getSetting } from '../../../../utils/settings'; import { FileSelectionType } from '../../../modals/filepicker'; +import WarnThirdParty from '../../../modals/WarnThirdParty'; +import { WarnThirdPartyType } from '../../../../utils/globalTypes'; 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..7e92e76bf 100644 --- a/frontend/src/components/settings/pages/general/StoreSelect.tsx +++ b/frontend/src/components/settings/pages/general/StoreSelect.tsx @@ -1,4 +1,4 @@ -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'; @@ -6,12 +6,16 @@ import { FaShapes } from 'react-icons/fa'; import Logger from '../../../../logger'; import { Store } from '../../../../store'; import { useSetting } from '../../../../utils/hooks/useSetting'; +import WarnThirdParty from '../../../modals/WarnThirdParty'; +import { WarnThirdPartyType } from '../../../../utils/globalTypes'; 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..37f2f4889 100644 --- a/frontend/src/utils/TranslationHelper.tsx +++ b/frontend/src/utils/TranslationHelper.tsx @@ -1,6 +1,7 @@ import { FC } from 'react'; import { Translation } from 'react-i18next'; +import { WarnThirdPartyType } from './globalTypes'; import Logger from '../logger'; import { InstallType } from '../plugin'; @@ -8,6 +9,7 @@ export enum TranslationClass { PLUGIN_LOADER = 'PluginLoader', PLUGIN_INSTALL_MODAL = 'PluginInstallModal', DEVELOPER = 'Developer', + WARN_THIRD_PARTY = 'WarnThirdParty', } interface TranslationHelperProps { @@ -15,11 +17,12 @@ 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 +50,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..8f1a1fa09 --- /dev/null +++ b/frontend/src/utils/globalTypes.ts @@ -0,0 +1,4 @@ +export enum WarnThirdPartyType { + REPO = 0, + ZIP = 1, + } \ No newline at end of file