summaryrefslogtreecommitdiff
path: root/frontend/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components')
-rw-r--r--frontend/src/components/DeckyIcon.tsx74
-rw-r--r--frontend/src/components/DeckyToaster.tsx57
-rw-r--r--frontend/src/components/DeckyToasterState.tsx69
-rw-r--r--frontend/src/components/TitleView.tsx10
-rw-r--r--frontend/src/components/Toast.tsx102
-rw-r--r--frontend/src/components/modals/filepicker/patches/index.ts8
-rw-r--r--frontend/src/components/settings/pages/testing/index.tsx7
7 files changed, 131 insertions, 196 deletions
diff --git a/frontend/src/components/DeckyIcon.tsx b/frontend/src/components/DeckyIcon.tsx
index 515bd847..f07f46d3 100644
--- a/frontend/src/components/DeckyIcon.tsx
+++ b/frontend/src/components/DeckyIcon.tsx
@@ -1,37 +1,39 @@
-export default function DeckyIcon() {
- return (
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 456" width="512" height="456">
- <g>
- <path
- style={{ fill: 'none' }}
- d="M154.33,72.51v49.79c11.78-0.17,23.48,2,34.42,6.39c10.93,4.39,20.89,10.91,29.28,19.18
- c8.39,8.27,15.06,18.13,19.61,29c4.55,10.87,6.89,22.54,6.89,34.32c0,11.78-2.34,23.45-6.89,34.32
- c-4.55,10.87-11.21,20.73-19.61,29c-8.39,8.27-18.35,14.79-29.28,19.18c-10.94,4.39-22.63,6.56-34.42,6.39v49.77
- c36.78,0,72.05-14.61,98.05-40.62c26-26.01,40.61-61.28,40.61-98.05c0-36.78-14.61-72.05-40.61-98.05
- C226.38,87.12,191.11,72.51,154.33,72.51z"
- />
+import { FC, SVGAttributes } from 'react';
- <ellipse
- transform="matrix(0.982 -0.1891 0.1891 0.982 -37.1795 32.9988)"
- style={{ fill: 'none' }}
- cx="154.33"
- cy="211.33"
- rx="69.33"
- ry="69.33"
- />
- <path style={{ fill: 'none' }} d="M430,97h-52v187h52c7.18,0,13-5.82,13-13V110C443,102.82,437.18,97,430,97z" />
- <path
- style={{ fill: 'currentColor' }}
- d="M432,27h-54V0H0v361c0,52.47,42.53,95,95,95h188c52.47,0,95-42.53,95-95v-7h54c44.18,0,80-35.82,80-80V107
- C512,62.82,476.18,27,432,27z M85,211.33c0-38.29,31.04-69.33,69.33-69.33c38.29,0,69.33,31.04,69.33,69.33
- c0,38.29-31.04,69.33-69.33,69.33C116.04,280.67,85,249.62,85,211.33z M252.39,309.23c-26.01,26-61.28,40.62-98.05,40.62v-49.77
- c11.78,0.17,23.48-2,34.42-6.39c10.93-4.39,20.89-10.91,29.28-19.18c8.39-8.27,15.06-18.13,19.61-29
- c4.55-10.87,6.89-22.53,6.89-34.32c0-11.78-2.34-23.45-6.89-34.32c-4.55-10.87-11.21-20.73-19.61-29
- c-8.39-8.27-18.35-14.79-29.28-19.18c-10.94-4.39-22.63-6.56-34.42-6.39V72.51c36.78,0,72.05,14.61,98.05,40.61
- c26,26.01,40.61,61.28,40.61,98.05C293,247.96,278.39,283.23,252.39,309.23z M443,271c0,7.18-5.82,13-13,13h-52V97h52
- c7.18,0,13,5.82,13,13V271z"
- />
- </g>
- </svg>
- );
-}
+const DeckyIcon: FC<SVGAttributes<SVGElement>> = (props) => (
+ <svg xmlns="http://www.w3.org/2000/svg" height="100%" width="100%" viewBox="0 0 512 456" {...props}>
+ <g>
+ <path
+ style={{ fill: 'none' }}
+ d="M154.33,72.51v49.79c11.78-0.17,23.48,2,34.42,6.39c10.93,4.39,20.89,10.91,29.28,19.18
+ c8.39,8.27,15.06,18.13,19.61,29c4.55,10.87,6.89,22.54,6.89,34.32c0,11.78-2.34,23.45-6.89,34.32
+ c-4.55,10.87-11.21,20.73-19.61,29c-8.39,8.27-18.35,14.79-29.28,19.18c-10.94,4.39-22.63,6.56-34.42,6.39v49.77
+ c36.78,0,72.05-14.61,98.05-40.62c26-26.01,40.61-61.28,40.61-98.05c0-36.78-14.61-72.05-40.61-98.05
+ C226.38,87.12,191.11,72.51,154.33,72.51z"
+ />
+
+ <ellipse
+ transform="matrix(0.982 -0.1891 0.1891 0.982 -37.1795 32.9988)"
+ style={{ fill: 'none' }}
+ cx="154.33"
+ cy="211.33"
+ rx="69.33"
+ ry="69.33"
+ />
+ <path style={{ fill: 'none' }} d="M430,97h-52v187h52c7.18,0,13-5.82,13-13V110C443,102.82,437.18,97,430,97z" />
+ <path
+ style={{ fill: 'currentColor' }}
+ d="M432,27h-54V0H0v361c0,52.47,42.53,95,95,95h188c52.47,0,95-42.53,95-95v-7h54c44.18,0,80-35.82,80-80V107
+ C512,62.82,476.18,27,432,27z M85,211.33c0-38.29,31.04-69.33,69.33-69.33c38.29,0,69.33,31.04,69.33,69.33
+ c0,38.29-31.04,69.33-69.33,69.33C116.04,280.67,85,249.62,85,211.33z M252.39,309.23c-26.01,26-61.28,40.62-98.05,40.62v-49.77
+ c11.78,0.17,23.48-2,34.42-6.39c10.93-4.39,20.89-10.91,29.28-19.18c8.39-8.27,15.06-18.13,19.61-29
+ c4.55-10.87,6.89-22.53,6.89-34.32c0-11.78-2.34-23.45-6.89-34.32c-4.55-10.87-11.21-20.73-19.61-29
+ c-8.39-8.27-18.35-14.79-29.28-19.18c-10.94-4.39-22.63-6.56-34.42-6.39V72.51c36.78,0,72.05,14.61,98.05,40.61
+ c26,26.01,40.61,61.28,40.61,98.05C293,247.96,278.39,283.23,252.39,309.23z M443,271c0,7.18-5.82,13-13,13h-52V97h52
+ c7.18,0,13,5.82,13,13V271z"
+ />
+ </g>
+ </svg>
+);
+
+export default DeckyIcon;
diff --git a/frontend/src/components/DeckyToaster.tsx b/frontend/src/components/DeckyToaster.tsx
deleted file mode 100644
index 056f1dd7..00000000
--- a/frontend/src/components/DeckyToaster.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import type { ToastData } from '@decky/api';
-import { joinClassNames } from '@decky/ui';
-import { FC, ReactElement, useEffect, useState } from 'react';
-
-import { useDeckyToasterState } from './DeckyToasterState';
-import Toast, { toastClasses } from './Toast';
-
-interface DeckyToasterProps {}
-
-interface RenderedToast {
- component: ReactElement;
- data: ToastData;
-}
-
-const DeckyToaster: FC<DeckyToasterProps> = () => {
- const { toasts, removeToast } = useDeckyToasterState();
- const [renderedToast, setRenderedToast] = useState<RenderedToast | null>(null);
- console.log(toasts);
- if (toasts.size > 0) {
- const [activeToast] = toasts;
- if (!renderedToast || activeToast != renderedToast.data) {
- // TODO play toast soundReactElement
- console.log('rendering toast', activeToast);
- setRenderedToast({ component: <Toast key={Math.random()} toast={activeToast} />, data: activeToast });
- }
- } else {
- if (renderedToast) setRenderedToast(null);
- }
- useEffect(() => {
- // not actually node but TS is shit
- let interval: number | null;
- if (renderedToast) {
- interval = setTimeout(
- () => {
- interval = null;
- console.log('clear toast', renderedToast.data);
- removeToast(renderedToast.data);
- },
- (renderedToast.data.duration || 5e3) + 1000,
- );
- console.log('set int', interval);
- }
- return () => {
- if (interval) {
- console.log('clearing int', interval);
- clearTimeout(interval);
- }
- };
- }, [renderedToast]);
- return (
- <div className={joinClassNames('deckyToaster', toastClasses.ToastPlaceholder)}>
- {renderedToast && renderedToast.component}
- </div>
- );
-};
-
-export default DeckyToaster;
diff --git a/frontend/src/components/DeckyToasterState.tsx b/frontend/src/components/DeckyToasterState.tsx
deleted file mode 100644
index ebe90b23..00000000
--- a/frontend/src/components/DeckyToasterState.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import type { ToastData } from '@decky/api';
-import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
-
-interface PublicDeckyToasterState {
- toasts: Set<ToastData>;
-}
-
-export class DeckyToasterState {
- private _toasts: Set<ToastData> = new Set();
-
- public eventBus = new EventTarget();
-
- publicState(): PublicDeckyToasterState {
- return { toasts: this._toasts };
- }
-
- addToast(toast: ToastData) {
- this._toasts.add(toast);
- this.notifyUpdate();
- }
-
- removeToast(toast: ToastData) {
- this._toasts.delete(toast);
- this.notifyUpdate();
- }
-
- private notifyUpdate() {
- this.eventBus.dispatchEvent(new Event('update'));
- }
-}
-
-interface DeckyToasterContext extends PublicDeckyToasterState {
- addToast(toast: ToastData): void;
- removeToast(toast: ToastData): void;
-}
-
-const DeckyToasterContext = createContext<DeckyToasterContext>(null as any);
-
-export const useDeckyToasterState = () => useContext(DeckyToasterContext);
-
-interface Props {
- deckyToasterState: DeckyToasterState;
- children: ReactNode;
-}
-
-export const DeckyToasterStateContextProvider: FC<Props> = ({ children, deckyToasterState }) => {
- const [publicDeckyToasterState, setPublicDeckyToasterState] = useState<PublicDeckyToasterState>({
- ...deckyToasterState.publicState(),
- });
-
- useEffect(() => {
- function onUpdate() {
- setPublicDeckyToasterState({ ...deckyToasterState.publicState() });
- }
-
- deckyToasterState.eventBus.addEventListener('update', onUpdate);
-
- return () => deckyToasterState.eventBus.removeEventListener('update', onUpdate);
- }, []);
-
- const addToast = deckyToasterState.addToast.bind(deckyToasterState);
- const removeToast = deckyToasterState.removeToast.bind(deckyToasterState);
-
- return (
- <DeckyToasterContext.Provider value={{ ...publicDeckyToasterState, addToast, removeToast }}>
- {children}
- </DeckyToasterContext.Provider>
- );
-};
diff --git a/frontend/src/components/TitleView.tsx b/frontend/src/components/TitleView.tsx
index 8b45aae4..0cb82b7f 100644
--- a/frontend/src/components/TitleView.tsx
+++ b/frontend/src/components/TitleView.tsx
@@ -1,4 +1,4 @@
-import { DialogButton, Focusable, Router, staticClasses } from '@decky/ui';
+import { DialogButton, Focusable, Navigation, staticClasses } from '@decky/ui';
import { CSSProperties, FC } from 'react';
import { useTranslation } from 'react-i18next';
import { BsGearFill } from 'react-icons/bs';
@@ -19,13 +19,13 @@ const TitleView: FC = () => {
const { t } = useTranslation();
const onSettingsClick = () => {
- Router.CloseSideMenus();
- Router.Navigate('/decky/settings');
+ Navigation.Navigate('/decky/settings');
+ Navigation.CloseSideMenus();
};
const onStoreClick = () => {
- Router.CloseSideMenus();
- Router.Navigate('/decky/store');
+ Navigation.Navigate('/decky/store');
+ Navigation.CloseSideMenus();
};
if (activePlugin === null) {
diff --git a/frontend/src/components/Toast.tsx b/frontend/src/components/Toast.tsx
index 79e3d864..e86e9337 100644
--- a/frontend/src/components/Toast.tsx
+++ b/frontend/src/components/Toast.tsx
@@ -1,37 +1,38 @@
import type { ToastData } from '@decky/api';
-import { findModule, joinClassNames } from '@decky/ui';
-import { FunctionComponent } from 'react';
+import { Focusable, Navigation, findClassModule, joinClassNames } from '@decky/ui';
+import { FC, memo } from 'react';
-interface ToastProps {
- toast: ToastData;
-}
+import Logger from '../logger';
-export const toastClasses = findModule((mod) => {
- if (typeof mod !== 'object') return false;
+const logger = new Logger('ToastRenderer');
- if (mod.ToastPlaceholder) {
- return true;
- }
+// TODO there are more of these
+export enum ToastLocation {
+ /** Big Picture popup toasts */
+ GAMEPADUI_POPUP = 1,
+ /** QAM Notifications tab */
+ GAMEPADUI_QAM = 3,
+}
- return false;
-});
+interface ToastProps {
+ toast: ToastData;
+ newIndicator?: boolean;
+}
-const templateClasses = findModule((mod) => {
- if (typeof mod !== 'object') return false;
+interface ToastRendererProps extends ToastProps {
+ location: ToastLocation;
+}
- if (mod.ShortTemplate) {
- return true;
- }
+const templateClasses = findClassModule((m) => m.ShortTemplate) || {};
- return false;
-});
+// These are memoized as they like to randomly rerender
-const Toast: FunctionComponent<ToastProps> = ({ toast }) => {
+const GamepadUIPopupToast: FC<Omit<ToastProps, 'newIndicator'>> = memo(({ toast }) => {
return (
<div
style={{ '--toast-duration': `${toast.duration}ms` } as React.CSSProperties}
onClick={toast.onClick}
- className={joinClassNames(templateClasses.ShortTemplate, toast.className || '')}
+ className={joinClassNames(templateClasses.ShortTemplate, toast.className || '', 'DeckyGamepadUIPopupToast')}
>
{toast.logo && <div className={templateClasses.StandardLogoDimensions}>{toast.logo}</div>}
<div className={joinClassNames(templateClasses.Content, toast.contentClassName || '')}>
@@ -43,6 +44,61 @@ const Toast: FunctionComponent<ToastProps> = ({ toast }) => {
</div>
</div>
);
-};
+});
+
+const GamepadUIQAMToast: FC<ToastProps> = memo(({ toast, newIndicator }) => {
+ // The fields aren't mismatched, the logic for these is just a bit weird.
+ return (
+ <Focusable
+ onActivate={() => {
+ toast.onClick?.();
+ Navigation.CloseSideMenus();
+ }}
+ className={joinClassNames(
+ templateClasses.StandardTemplateContainer,
+ toast.className || '',
+ 'DeckyGamepadUIQAMToast',
+ )}
+ >
+ <div className={templateClasses.StandardTemplate}>
+ {toast.logo && <div className={templateClasses.StandardLogoDimensions}>{toast.logo}</div>}
+ <div className={joinClassNames(templateClasses.Content, toast.contentClassName || '')}>
+ <div className={templateClasses.Header}>
+ {toast.icon && <div className={templateClasses.Icon}>{toast.icon}</div>}
+ {toast.title && <div className={templateClasses.Title}>{toast.title}</div>}
+ {/* timestamp should always be defined by toaster */}
+ {/* TODO check how valve does this */}
+ {toast.timestamp && (
+ <div className={templateClasses.Timestamp}>
+ {toast.timestamp.toLocaleTimeString(undefined, { timeStyle: 'short' })}
+ </div>
+ )}
+ </div>
+ {toast.body && <div className={templateClasses.StandardNotificationDescription}>{toast.body}</div>}
+ {toast.subtext && <div className={templateClasses.StandardNotificationSubText}>{toast.subtext}</div>}
+ </div>
+ {newIndicator && (
+ <div className={templateClasses.NewIndicator}>
+ <svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 50 50" fill="none">
+ <circle fill="currentColor" cx="25" cy="25" r="25"></circle>
+ </svg>
+ </div>
+ )}
+ </div>
+ </Focusable>
+ );
+});
+
+export const ToastRenderer: FC<ToastRendererProps> = memo(({ toast, location, newIndicator }) => {
+ switch (location) {
+ default:
+ logger.warn(`Toast UI not implemented for location ${location}! Falling back to GamepadUIQAMToast.`);
+ return <GamepadUIQAMToast toast={toast} newIndicator={false} />;
+ case ToastLocation.GAMEPADUI_POPUP:
+ return <GamepadUIPopupToast toast={toast} />;
+ case ToastLocation.GAMEPADUI_QAM:
+ return <GamepadUIQAMToast toast={toast} newIndicator={newIndicator} />;
+ }
+});
-export default Toast;
+export default ToastRenderer;
diff --git a/frontend/src/components/modals/filepicker/patches/index.ts b/frontend/src/components/modals/filepicker/patches/index.ts
index 310bfbf8..fa0f0bb0 100644
--- a/frontend/src/components/modals/filepicker/patches/index.ts
+++ b/frontend/src/components/modals/filepicker/patches/index.ts
@@ -1,10 +1,10 @@
-import library from './library';
-let patches: Function[] = [];
+// import library from './library';
+// let patches: Function[] = [];
export function deinitFilepickerPatches() {
- patches.forEach((unpatch) => unpatch());
+ // patches.forEach((unpatch) => unpatch());
}
export async function initFilepickerPatches() {
- patches.push(await library());
+ // patches.push(await library());
}
diff --git a/frontend/src/components/settings/pages/testing/index.tsx b/frontend/src/components/settings/pages/testing/index.tsx
index eb572614..8f02c207 100644
--- a/frontend/src/components/settings/pages/testing/index.tsx
+++ b/frontend/src/components/settings/pages/testing/index.tsx
@@ -10,7 +10,7 @@ import {
} from '@decky/ui';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { FaDownload, FaInfo } from 'react-icons/fa';
+import { FaDownload, FaFlask, FaInfo } from 'react-icons/fa';
import { setSetting } from '../../../../utils/settings';
import { UpdateBranch } from '../general/BranchSelect';
@@ -91,17 +91,20 @@ export default function TestingVersionList() {
<DialogButton
style={{ height: '40px', minWidth: '60px', marginRight: '10px' }}
onClick={async () => {
- DeckyPluginLoader.toaster.toast({
+ const downloadToast = DeckyPluginLoader.toaster.toast({
title: t('Testing.start_download_toast', { id: version.id }),
body: null,
+ icon: <FaFlask />,
});
try {
await downloadTestingVersion(version.id, version.head_sha);
+ downloadToast.dismiss();
} catch (e) {
if (e instanceof Error) {
DeckyPluginLoader.toaster.toast({
title: t('Testing.error'),
body: `${e.name}: ${e.message}`,
+ icon: <FaFlask />,
});
}
}