summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAAGaming <aagaming@riseup.net>2026-10-17 20:31:24 -0400
committerAAGaming <aagaming@riseup.net>2026-10-17 20:31:24 -0400
commita349af9d76d0457067fb546872304df8cdb6ab27 (patch)
treea2376f6b6288a4c79dea62d90510b09051ce73fb
parentdae497ef949d4b36b2bdc3537f4f52b1cfdafa2e (diff)
parent82df584b461fe9a283b687ad9d883ea1cb8767a8 (diff)
downloaddecky-loader-a349af9d76d0457067fb546872304df8cdb6ab27.tar.gz
decky-loader-a349af9d76d0457067fb546872304df8cdb6ab27.zip
Merge branch 'main' of github.com:SteamDeckHomebrew/decky-loader
-rw-r--r--backend/decky_loader/helpers.py5
-rw-r--r--backend/decky_loader/loader.py5
-rw-r--r--backend/decky_loader/locales/en-US.json9
-rw-r--r--backend/decky_loader/localplatform/localplatformmac.py10
-rw-r--r--frontend/src/components/modals/WarnThirdParty.tsx78
-rw-r--r--frontend/src/components/settings/pages/developer/index.tsx29
-rw-r--r--frontend/src/components/settings/pages/general/StoreSelect.tsx44
-rw-r--r--frontend/src/utils/TranslationHelper.tsx29
-rw-r--r--frontend/src/utils/globalTypes.ts4
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,
+}