From 131f0961ff451ec47376483178e092c8d7403b27 Mon Sep 17 00:00:00 2001 From: AAGaming Date: Mon, 5 Aug 2024 14:07:10 -0400 Subject: Rewrite router/tabs/toaster hooks (#661) --- frontend/src/components/DeckyIcon.tsx | 74 +++++++-------- frontend/src/components/DeckyToaster.tsx | 57 ------------ frontend/src/components/DeckyToasterState.tsx | 69 -------------- frontend/src/components/TitleView.tsx | 10 +- frontend/src/components/Toast.tsx | 102 ++++++++++++++++----- .../components/modals/filepicker/patches/index.ts | 8 +- .../components/settings/pages/testing/index.tsx | 7 +- 7 files changed, 131 insertions(+), 196 deletions(-) delete mode 100644 frontend/src/components/DeckyToaster.tsx delete mode 100644 frontend/src/components/DeckyToasterState.tsx (limited to 'frontend/src/components') 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 ( - - - +import { FC, SVGAttributes } from 'react'; - - - - - - ); -} +const DeckyIcon: FC> = (props) => ( + + + + + + + + + +); + +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 = () => { - const { toasts, removeToast } = useDeckyToasterState(); - const [renderedToast, setRenderedToast] = useState(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: , 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 ( -
- {renderedToast && renderedToast.component} -
- ); -}; - -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; -} - -export class DeckyToasterState { - private _toasts: Set = 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(null as any); - -export const useDeckyToasterState = () => useContext(DeckyToasterContext); - -interface Props { - deckyToasterState: DeckyToasterState; - children: ReactNode; -} - -export const DeckyToasterStateContextProvider: FC = ({ children, deckyToasterState }) => { - const [publicDeckyToasterState, setPublicDeckyToasterState] = useState({ - ...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 ( - - {children} - - ); -}; 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 = ({ toast }) => { +const GamepadUIPopupToast: FC> = memo(({ toast }) => { return (
{toast.logo &&
{toast.logo}
}
@@ -43,6 +44,61 @@ const Toast: FunctionComponent = ({ toast }) => {
); -}; +}); + +const GamepadUIQAMToast: FC = memo(({ toast, newIndicator }) => { + // The fields aren't mismatched, the logic for these is just a bit weird. + return ( + { + toast.onClick?.(); + Navigation.CloseSideMenus(); + }} + className={joinClassNames( + templateClasses.StandardTemplateContainer, + toast.className || '', + 'DeckyGamepadUIQAMToast', + )} + > +
+ {toast.logo &&
{toast.logo}
} +
+
+ {toast.icon &&
{toast.icon}
} + {toast.title &&
{toast.title}
} + {/* timestamp should always be defined by toaster */} + {/* TODO check how valve does this */} + {toast.timestamp && ( +
+ {toast.timestamp.toLocaleTimeString(undefined, { timeStyle: 'short' })} +
+ )} +
+ {toast.body &&
{toast.body}
} + {toast.subtext &&
{toast.subtext}
} +
+ {newIndicator && ( +
+ + + +
+ )} +
+
+ ); +}); + +export const ToastRenderer: FC = memo(({ toast, location, newIndicator }) => { + switch (location) { + default: + logger.warn(`Toast UI not implemented for location ${location}! Falling back to GamepadUIQAMToast.`); + return ; + case ToastLocation.GAMEPADUI_POPUP: + return ; + case ToastLocation.GAMEPADUI_QAM: + return ; + } +}); -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() { { - DeckyPluginLoader.toaster.toast({ + const downloadToast = DeckyPluginLoader.toaster.toast({ title: t('Testing.start_download_toast', { id: version.id }), body: null, + icon: , }); 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: , }); } } -- cgit v1.2.3