diff options
| -rw-r--r-- | backend/decky_loader/helpers.py | 5 | ||||
| -rw-r--r-- | backend/decky_loader/loader.py | 5 | ||||
| -rw-r--r-- | backend/decky_loader/locales/en-US.json | 9 | ||||
| -rw-r--r-- | backend/decky_loader/localplatform/localplatformmac.py | 10 | ||||
| -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 |
9 files changed, 184 insertions, 29 deletions
diff --git a/backend/decky_loader/helpers.py b/backend/decky_loader/helpers.py index 1621652f..bc330298 100644 --- a/backend/decky_loader/helpers.py +++ b/backend/decky_loader/helpers.py @@ -93,10 +93,11 @@ user_agent = f"Decky/{get_loader_version()} (https://decky.xyz)" # returns the appropriate system python paths def get_system_pythonpaths() -> list[str]: try: + is_linux_or_mac = localplatform.ON_LINUX or localplatform.ON_MAC # run as normal normal user if on linux to also include user python paths - proc = subprocess.run(["python3" if localplatform.ON_LINUX else "python", "-c", "import sys; print('\\n'.join(x for x in sys.path if x))"], + proc = subprocess.run(["python3" if is_linux_or_mac else "python", "-c", "import sys; print('\\n'.join(x for x in sys.path if x))"], # TODO make this less insane - capture_output=True, user=localplatform.localplatform._get_user_id() if localplatform.ON_LINUX else None, env={} if localplatform.ON_LINUX else None) # pyright: ignore [reportPrivateUsage] + capture_output=True, user=localplatform.localplatform._get_user_id() if is_linux_or_mac else None, env={} if is_linux_or_mac else None) # pyright: ignore [reportPrivateUsage] proc.check_returncode() diff --git a/backend/decky_loader/loader.py b/backend/decky_loader/loader.py index 297fc202..e7c2fec7 100644 --- a/backend/decky_loader/loader.py +++ b/backend/decky_loader/loader.py @@ -9,6 +9,7 @@ from typing import Any, Tuple, Dict, cast from aiohttp import web from os.path import exists from decky_loader.helpers import get_homebrew_path +from decky_loader.localplatform.localplatform import ON_MAC from watchdog.events import RegexMatchingEventHandler, FileSystemEvent from watchdog.observers import Observer @@ -106,6 +107,10 @@ class Loader: async def enable_reload_wait(self): if self.live_reload: + if ON_MAC: + self.logger.info("Hot reload is not supported on macOS") + return + await sleep(10) if self.watcher and self.live_reload: self.logger.info("Hot reload enabled") diff --git a/backend/decky_loader/locales/en-US.json b/backend/decky_loader/locales/en-US.json index 6b6553ad..76531ec4 100644 --- a/backend/decky_loader/locales/en-US.json +++ b/backend/decky_loader/locales/en-US.json @@ -296,6 +296,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/backend/decky_loader/localplatform/localplatformmac.py b/backend/decky_loader/localplatform/localplatformmac.py index fd31870b..fd491785 100644 --- a/backend/decky_loader/localplatform/localplatformmac.py +++ b/backend/decky_loader/localplatform/localplatformmac.py @@ -1,10 +1,12 @@ from ..enums import UserType -import os from . import localplatformlinux -# this should be public +# NOTE: localplatformlinux has these private functions that get used in helpers.py +def _get_user_id() -> int: # pyright: ignore[reportUnusedFunction] + return localplatformlinux._get_user_id() # pyright: ignore[reportPrivateUsage] + def _get_effective_user_id() -> int: # pyright: ignore[reportUnusedFunction] - return os.geteuid() + return localplatformlinux._get_effective_user_id() # pyright: ignore[reportPrivateUsage] def chown(path : str, user : UserType = UserType.HOST_USER, recursive : bool = True) -> bool: return localplatformlinux.chown(path, user, recursive) @@ -43,7 +45,7 @@ def get_username() -> str: return localplatformlinux.get_username() def get_privileged_path() -> str: - '''On Mac, privileged_path is equal to unprivileged_path''' + # On Mac, privileged_path is equal to unprivileged_path return get_unprivileged_path() def get_unprivileged_path() -> str: 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, +} |
