diff options
| author | AAGaming <aagaming@riseup.net> | 2024-08-05 14:07:10 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-08-05 14:07:10 -0400 |
| commit | 131f0961ff451ec47376483178e092c8d7403b27 (patch) | |
| tree | 4d2ea34e8220e14c4b820cc1ad38face7193f6fe /frontend/src/components | |
| parent | 75aa1e4851445646994ba3a61ff41325403359fb (diff) | |
| download | decky-loader-131f0961ff451ec47376483178e092c8d7403b27.tar.gz decky-loader-131f0961ff451ec47376483178e092c8d7403b27.zip | |
Rewrite router/tabs/toaster hooks (#661)
Diffstat (limited to 'frontend/src/components')
| -rw-r--r-- | frontend/src/components/DeckyIcon.tsx | 74 | ||||
| -rw-r--r-- | frontend/src/components/DeckyToaster.tsx | 57 | ||||
| -rw-r--r-- | frontend/src/components/DeckyToasterState.tsx | 69 | ||||
| -rw-r--r-- | frontend/src/components/TitleView.tsx | 10 | ||||
| -rw-r--r-- | frontend/src/components/Toast.tsx | 102 | ||||
| -rw-r--r-- | frontend/src/components/modals/filepicker/patches/index.ts | 8 | ||||
| -rw-r--r-- | frontend/src/components/settings/pages/testing/index.tsx | 7 |
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 />, }); } } |
