diff --git a/backend/locales/en-US.json b/backend/locales/en-US.json index 1d0b9f774..a4fb643df 100644 --- a/backend/locales/en-US.json +++ b/backend/locales/en-US.json @@ -264,6 +264,15 @@ "updating": "Updating" } }, + "WarnThirdParty":{ + "title_zip": "Third party plugin install", + "title_repo": "Third party store selection", + "button_processing_one": "Please wait {{timer}} second", + "button_processing_many": "Please wait {{timer}} seconds", + "button_idle": "I accept the risks", + "desc_zip": "This plugin that has been requested to be installed is taken outside our official store; we haven't audited it, so it will contain unvetted code; it might be doing exactly what it would be supposed to do or it might have additional features built in that will try to steal your data and take over your Steam account. The Decky team is not responsible for any problem arising from installing this plugin.", + "desc_repo": "This third party store you are installing is not our official store; The plugins it contains aren't being reviewed for security and stability by the Decky team; you should check with whoever is maintaining this store and decide for yourself if you trust them or not. The Decky team is not responsible for any problem arising from installing plugins from this third party store." + }, "Testing": { "download": "Download" } diff --git a/frontend/src/components/modals/WarnThirdParty.tsx b/frontend/src/components/modals/WarnThirdParty.tsx new file mode 100644 index 000000000..d6adf6abb --- /dev/null +++ b/frontend/src/components/modals/WarnThirdParty.tsx @@ -0,0 +1,82 @@ +import { ConfirmModal } from 'decky-frontend-lib'; +import { FC, useEffect, useState } from 'react'; +import { FaExclamationTriangle } from 'react-icons/fa'; + +import TranslationHelper, { TranslationClass } from '../../utils/TranslationHelper'; + +interface WarnThirdPartyProps { + seconds?: number; + type: WarnThirdPartyType; + onOK(): void; + onCancel(): void; + closeModal?(): void; +} + +export enum WarnThirdPartyType { + REPO = 0, + ZIP = 1, +} + +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 5ed765155..b6e5c18fe 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-frontend-lib'; import { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -18,6 +19,7 @@ import { installFromURL } from '../../../../store'; import { useSetting } from '../../../../utils/hooks/useSetting'; import { getSetting } from '../../../../utils/settings'; import { FileSelectionType } from '../../../modals/filepicker'; +import WarnThirdParty, { WarnThirdPartyType } from '../../../modals/WarnThirdParty'; import RemoteDebuggingSettings from '../general/RemoteDebugging'; const logger = new Logger('DeveloperIndex'); @@ -77,7 +79,20 @@ export default function DeveloperSettings() { } icon={} > - installFromURL(pluginURL)}> + + showModal( + { + installFromURL(pluginURL); + }} + onCancel={() => {}} + />, + ) + } + > {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 3cb80303e..4a04638c0 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-frontend-lib'; +import { Dropdown, Field, TextField, showModal } from 'decky-frontend-lib'; import { FunctionComponent } from 'react'; import { useTranslation } from 'react-i18next'; import { FaShapes } from 'react-icons/fa'; @@ -6,6 +6,7 @@ import { FaShapes } from 'react-icons/fa'; import Logger from '../../../../logger'; import { Store } from '../../../../store'; import { useSetting } from '../../../../utils/hooks/useSetting'; +import WarnThirdParty, { WarnThirdPartyType } from '../../../modals/WarnThirdParty'; const logger = new Logger('StoreSelect'); @@ -38,20 +39,27 @@ const StoreSelect: FunctionComponent<{}> = () => { }} /> - {selectedStore == Store.Custom && ( - setSelectedStoreURL(e?.target.value || null)} - /> - } - icon={} - > - )} + {selectedStore == Store.Custom && + showModal( + {}} + 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 99584d6be..95dcd7afb 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 '../components/modals/WarnThirdParty'; 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,6 +17,7 @@ interface TranslationHelperProps { trans_text: string; i18n_args?: {}; install_type?: number; + warn_type?: WarnThirdPartyType; } const logger = new Logger('TranslationHelper'); @@ -24,6 +27,7 @@ const TranslationHelper: FC = ({ trans_text, i18n_args = null, install_type = 0, + warn_type = WarnThirdPartyType.REPO, }) => { return ( @@ -52,6 +56,25 @@ const TranslationHelper: FC = ({ return i18n_args ? t(TranslationClass.DEVELOPER + '.' + trans_text, i18n_args) : t(TranslationClass.DEVELOPER + '.' + trans_text); + //Handle different messages in different class cases + case TranslationClass.WARN_THIRD_PARTY: + //Needed only for title and description + if (!trans_text.startsWith('button')) { + switch (warn_type) { + case WarnThirdPartyType.REPO: + return i18n_args + ? t(TranslationClass.WARN_THIRD_PARTY + '.' + trans_text + '_repo', i18n_args) + : t(TranslationClass.WARN_THIRD_PARTY + '.' + trans_text + '_repo'); + case WarnThirdPartyType.ZIP: + return i18n_args + ? t(TranslationClass.WARN_THIRD_PARTY + '.' + trans_text + '_zip', i18n_args) + : t(TranslationClass.WARN_THIRD_PARTY + '.' + trans_text + '_zip'); + } + } else { + return i18n_args + ? t(TranslationClass.WARN_THIRD_PARTY + '.' + trans_text, i18n_args) + : t(TranslationClass.WARN_THIRD_PARTY + '.' + trans_text); + } default: logger.error('We should never fall in the default case!'); return '';