summaryrefslogtreecommitdiff
path: root/frontend/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components')
-rw-r--r--frontend/src/components/DeckyErrorBoundary.tsx201
-rw-r--r--frontend/src/components/DeckyGlobalComponentsState.tsx3
-rw-r--r--frontend/src/components/DeckyRouterState.tsx3
-rw-r--r--frontend/src/components/DeckyState.tsx3
-rw-r--r--frontend/src/components/DeckyToaster.tsx5
-rw-r--r--frontend/src/components/DeckyToasterState.tsx2
-rw-r--r--frontend/src/components/Toast.tsx3
-rw-r--r--frontend/src/components/modals/DropdownMultiselect.tsx2
-rw-r--r--frontend/src/components/modals/PluginInstallModal.tsx28
9 files changed, 228 insertions, 22 deletions
diff --git a/frontend/src/components/DeckyErrorBoundary.tsx b/frontend/src/components/DeckyErrorBoundary.tsx
new file mode 100644
index 00000000..a851b2e1
--- /dev/null
+++ b/frontend/src/components/DeckyErrorBoundary.tsx
@@ -0,0 +1,201 @@
+import { sleep } from '@decky/ui';
+import { ErrorInfo, FunctionComponent, useReducer, useState } from 'react';
+
+import { uninstallPlugin } from '../plugin';
+import { doRestart, doShutdown } from '../updater';
+
+interface ReactErrorInfo {
+ error: Error;
+ info: ErrorInfo;
+}
+
+interface DeckyErrorBoundaryProps {
+ error: ReactErrorInfo;
+ errorKey: string;
+ reset: () => void;
+}
+
+declare global {
+ interface Window {
+ SystemNetworkStore?: any;
+ }
+}
+
+const pluginErrorRegex = /\(http:\/\/localhost:1337\/plugins\/(.*)\//;
+const pluginSourceMapErrorRegex = /\(decky:\/\/decky\/plugin\/(.*)\//;
+const legacyPluginErrorRegex = /\(decky:\/\/decky\/legacy_plugin\/(.*)\/index.js/;
+
+function getLikelyErrorSource(error: ReactErrorInfo): [source: string, wasPlugin: boolean] {
+ const pluginMatch = error.error.stack?.match(pluginErrorRegex);
+ if (pluginMatch) {
+ return [decodeURIComponent(pluginMatch[1]), true];
+ }
+
+ const pluginMatchViaMap = error.error.stack?.match(pluginSourceMapErrorRegex);
+ if (pluginMatchViaMap) {
+ return [decodeURIComponent(pluginMatchViaMap[1]), true];
+ }
+
+ const legacyPluginMatch = error.error.stack?.match(legacyPluginErrorRegex);
+ if (legacyPluginMatch) {
+ return [decodeURIComponent(legacyPluginMatch[1]), true];
+ }
+
+ if (error.error.stack?.includes('http://localhost:1337/')) {
+ return ['the Decky frontend', false];
+ }
+ return ['Steam', false];
+}
+
+export const startSSH = DeckyBackend.callable('utilities/start_ssh');
+export const starrCEFForwarding = DeckyBackend.callable('utilities/allow_remote_debugging');
+
+function ipToString(ip: number) {
+ return [(ip >>> 24) & 255, (ip >>> 16) & 255, (ip >>> 8) & 255, (ip >>> 0) & 255].join('.');
+}
+
+// Intentionally not localized since we can't really trust React here
+const DeckyErrorBoundary: FunctionComponent<DeckyErrorBoundaryProps> = ({ error, reset }) => {
+ const [actionLog, addLogLine] = useReducer((log: string, line: string) => (log += '\n' + line), '');
+ const [actionsEnabled, setActionsEnabled] = useState<boolean>(true);
+ const [debugAllowed, setDebugAllowed] = useState<boolean>(true);
+ const [errorSource, wasCausedByPlugin] = getLikelyErrorSource(error);
+
+ return (
+ <div
+ style={{
+ overflow: 'scroll',
+ marginLeft: '15px',
+ color: 'white',
+ fontSize: '16px',
+ userSelect: 'auto',
+ backgroundColor: 'black',
+ marginTop: '48px', // Incase this is a page
+ }}
+ >
+ <h1
+ style={{
+ fontSize: '20px',
+ display: 'inline-block',
+ marginTop: '15px',
+ userSelect: 'auto',
+ }}
+ >
+ ⚠️ An error occured rendering this content.
+ </h1>
+ <p>This error likely occured in {getLikelyErrorSource(error)}.</p>
+ {actionLog?.length > 0 && (
+ <pre>
+ <code>
+ Running actions...
+ {actionLog}
+ </code>
+ </pre>
+ )}
+ {actionsEnabled && (
+ <>
+ <h3>Actions: </h3>
+ <p>Use the touch screen.</p>
+ <div style={{ display: 'block', marginBottom: '5px' }}>
+ <button style={{ marginRight: '5px', padding: '5px' }} onClick={reset}>
+ Retry
+ </button>
+ <button style={{ marginRight: '5px', padding: '5px' }} onClick={() => SteamClient.User.StartRestart()}>
+ Restart Steam
+ </button>
+ </div>
+ <div style={{ display: 'block', marginBottom: '5px' }}>
+ <button
+ style={{ marginRight: '5px', padding: '5px' }}
+ onClick={async () => {
+ setActionsEnabled(false);
+ addLogLine('Restarting Decky...');
+ doRestart();
+ await sleep(2000);
+ addLogLine('Reloading UI...');
+ }}
+ >
+ Restart Decky
+ </button>
+ <button
+ style={{ marginRight: '5px', padding: '5px' }}
+ onClick={async () => {
+ setActionsEnabled(false);
+ addLogLine('Stopping Decky...');
+ doShutdown();
+ await sleep(5000);
+ addLogLine('Restarting Steam...');
+ SteamClient.User.StartRestart();
+ }}
+ >
+ Disable Decky until next boot
+ </button>
+ </div>
+ {debugAllowed && (
+ <div style={{ display: 'block', marginBottom: '5px' }}>
+ <button
+ style={{ marginRight: '5px', padding: '5px' }}
+ onClick={async () => {
+ setDebugAllowed(false);
+ addLogLine('Enabling CEF debugger forwarding...');
+ await starrCEFForwarding();
+ addLogLine('Enabling SSH...');
+ await startSSH();
+ addLogLine('Ready for debugging!');
+ if (window?.SystemNetworkStore?.wirelessNetworkDevice?.ip4?.addresses?.[0]?.ip) {
+ const ip = ipToString(window.SystemNetworkStore.wirelessNetworkDevice.ip4.addresses[0].ip);
+ addLogLine(`CEF Debugger: http://${ip}:8081`);
+ addLogLine(`SSH: deck@${ip}`);
+ }
+ }}
+ >
+ Allow remote debugging and SSH until next boot
+ </button>
+ </div>
+ )}
+ {wasCausedByPlugin && (
+ <div style={{ display: 'block', marginBottom: '5px' }}>
+ {'\n'}
+ <button
+ style={{ marginRight: '5px', padding: '5px' }}
+ onClick={async () => {
+ setActionsEnabled(false);
+ addLogLine(`Uninstalling ${errorSource}...`);
+ await uninstallPlugin(errorSource);
+ await DeckyPluginLoader.frozenPluginsService.invalidate();
+ await DeckyPluginLoader.hiddenPluginsService.invalidate();
+ await sleep(1000);
+ addLogLine('Restarting Decky...');
+ doRestart();
+ await sleep(2000);
+ addLogLine('Restarting Steam...');
+ await sleep(500);
+ SteamClient.User.StartRestart();
+ }}
+ >
+ Uninstall {errorSource} and restart Decky
+ </button>
+ </div>
+ )}
+ </>
+ )}
+
+ <pre
+ style={{
+ marginTop: '15px',
+ opacity: 0.7,
+ userSelect: 'auto',
+ }}
+ >
+ <code>
+ {error.error.stack}
+ {'\n\n'}
+ Component Stack:
+ {error.info.componentStack}
+ </code>
+ </pre>
+ </div>
+ );
+};
+
+export default DeckyErrorBoundary;
diff --git a/frontend/src/components/DeckyGlobalComponentsState.tsx b/frontend/src/components/DeckyGlobalComponentsState.tsx
index fe45588b..475d1e4a 100644
--- a/frontend/src/components/DeckyGlobalComponentsState.tsx
+++ b/frontend/src/components/DeckyGlobalComponentsState.tsx
@@ -1,4 +1,4 @@
-import { FC, createContext, useContext, useEffect, useState } from 'react';
+import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
interface PublicDeckyGlobalComponentsState {
components: Map<string, FC>;
@@ -40,6 +40,7 @@ export const useDeckyGlobalComponentsState = () => useContext(DeckyGlobalCompone
interface Props {
deckyGlobalComponentsState: DeckyGlobalComponentsState;
+ children: ReactNode;
}
export const DeckyGlobalComponentsStateContextProvider: FC<Props> = ({
diff --git a/frontend/src/components/DeckyRouterState.tsx b/frontend/src/components/DeckyRouterState.tsx
index 0c8bb1ba..426ed731 100644
--- a/frontend/src/components/DeckyRouterState.tsx
+++ b/frontend/src/components/DeckyRouterState.tsx
@@ -1,4 +1,4 @@
-import { ComponentType, FC, createContext, useContext, useEffect, useState } from 'react';
+import { ComponentType, FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
import type { RouteProps } from 'react-router';
export interface RouterEntry {
@@ -71,6 +71,7 @@ export const useDeckyRouterState = () => useContext(DeckyRouterStateContext);
interface Props {
deckyRouterState: DeckyRouterState;
+ children: ReactNode;
}
export const DeckyRouterStateContextProvider: FC<Props> = ({ children, deckyRouterState }) => {
diff --git a/frontend/src/components/DeckyState.tsx b/frontend/src/components/DeckyState.tsx
index 749e27ce..75106e62 100644
--- a/frontend/src/components/DeckyState.tsx
+++ b/frontend/src/components/DeckyState.tsx
@@ -1,4 +1,4 @@
-import { FC, createContext, useContext, useEffect, useState } from 'react';
+import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
import { DEFAULT_NOTIFICATION_SETTINGS, NotificationSettings } from '../notification-service';
import { Plugin } from '../plugin';
@@ -134,6 +134,7 @@ export const useDeckyState = () => useContext(DeckyStateContext);
interface Props {
deckyState: DeckyState;
+ children?: ReactNode;
}
export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) => {
diff --git a/frontend/src/components/DeckyToaster.tsx b/frontend/src/components/DeckyToaster.tsx
index 00b7b4db..1cb51d65 100644
--- a/frontend/src/components/DeckyToaster.tsx
+++ b/frontend/src/components/DeckyToaster.tsx
@@ -1,4 +1,5 @@
-import { ToastData, joinClassNames } from '@decky/ui';
+import type { ToastData } from '@decky/api';
+import { joinClassNames } from '@decky/ui';
import { FC, useEffect, useState } from 'react';
import { ReactElement } from 'react-markdown/lib/react-markdown';
@@ -28,7 +29,7 @@ const DeckyToaster: FC<DeckyToasterProps> = () => {
}
useEffect(() => {
// not actually node but TS is shit
- let interval: NodeJS.Timer | null;
+ let interval: NodeJS.Timeout | number | null;
if (renderedToast) {
interval = setTimeout(
() => {
diff --git a/frontend/src/components/DeckyToasterState.tsx b/frontend/src/components/DeckyToasterState.tsx
index d6c3871f..8d0a5d45 100644
--- a/frontend/src/components/DeckyToasterState.tsx
+++ b/frontend/src/components/DeckyToasterState.tsx
@@ -1,4 +1,4 @@
-import { ToastData } from '@decky/ui';
+import type { ToastData } from '@decky/api';
import { FC, createContext, useContext, useEffect, useState } from 'react';
interface PublicDeckyToasterState {
diff --git a/frontend/src/components/Toast.tsx b/frontend/src/components/Toast.tsx
index ab01671a..79e3d864 100644
--- a/frontend/src/components/Toast.tsx
+++ b/frontend/src/components/Toast.tsx
@@ -1,4 +1,5 @@
-import { ToastData, findModule, joinClassNames } from '@decky/ui';
+import type { ToastData } from '@decky/api';
+import { findModule, joinClassNames } from '@decky/ui';
import { FunctionComponent } from 'react';
interface ToastProps {
diff --git a/frontend/src/components/modals/DropdownMultiselect.tsx b/frontend/src/components/modals/DropdownMultiselect.tsx
index 4c5cf7b1..255c6fa0 100644
--- a/frontend/src/components/modals/DropdownMultiselect.tsx
+++ b/frontend/src/components/modals/DropdownMultiselect.tsx
@@ -59,7 +59,7 @@ const DropdownMultiselect: FC<{
const [itemsSelected, setItemsSelected] = useState<any>(selected);
const { t } = useTranslation();
- const handleItemSelect = useCallback((checked, value) => {
+ const handleItemSelect = useCallback((checked: boolean, value: any) => {
setItemsSelected((x: any) =>
checked ? [...x.filter((y: any) => y !== value), value] : x.filter((y: any) => y !== value),
);
diff --git a/frontend/src/components/modals/PluginInstallModal.tsx b/frontend/src/components/modals/PluginInstallModal.tsx
index 8b3128a1..c6c90264 100644
--- a/frontend/src/components/modals/PluginInstallModal.tsx
+++ b/frontend/src/components/modals/PluginInstallModal.tsx
@@ -60,10 +60,10 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
strTitle={
<div>
<TranslationHelper
- trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
- trans_text="title"
- i18n_args={{ artifact: artifact }}
- install_type={installType}
+ transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
+ transText="title"
+ i18nArgs={{ artifact: artifact }}
+ installType={installType}
/>
</div>
}
@@ -71,17 +71,17 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
loading ? (
<div>
<TranslationHelper
- trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
- trans_text="button_processing"
- install_type={installType}
+ transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
+ transText="button_processing"
+ installType={installType}
/>
</div>
) : (
<div>
<TranslationHelper
- trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
- trans_text="button_idle"
- install_type={installType}
+ transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
+ transText="button_idle"
+ installType={installType}
/>
</div>
)
@@ -89,13 +89,13 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
>
<div>
<TranslationHelper
- trans_class={TranslationClass.PLUGIN_INSTALL_MODAL}
- trans_text="desc"
- i18n_args={{
+ transClass={TranslationClass.PLUGIN_INSTALL_MODAL}
+ transText="desc"
+ i18nArgs={{
artifact: artifact,
version: version,
}}
- install_type={installType}
+ installType={installType}
/>
</div>
{loading && (