diff options
Diffstat (limited to 'frontend/src/components')
| -rw-r--r-- | frontend/src/components/DeckyGlobalComponentsState.tsx | 74 | ||||
| -rw-r--r-- | frontend/src/components/DeckyToaster.tsx | 54 | ||||
| -rw-r--r-- | frontend/src/components/DeckyToasterState.tsx | 69 | ||||
| -rw-r--r-- | frontend/src/components/Markdown.tsx | 4 | ||||
| -rw-r--r-- | frontend/src/components/QuickAccessVisibleState.tsx | 28 | ||||
| -rw-r--r-- | frontend/src/components/Toast.tsx | 27 | ||||
| -rw-r--r-- | frontend/src/components/settings/pages/general/Updater.tsx | 9 |
7 files changed, 237 insertions, 28 deletions
diff --git a/frontend/src/components/DeckyGlobalComponentsState.tsx b/frontend/src/components/DeckyGlobalComponentsState.tsx new file mode 100644 index 00000000..fe45588b --- /dev/null +++ b/frontend/src/components/DeckyGlobalComponentsState.tsx @@ -0,0 +1,74 @@ +import { FC, createContext, useContext, useEffect, useState } from 'react'; + +interface PublicDeckyGlobalComponentsState { + components: Map<string, FC>; +} + +export class DeckyGlobalComponentsState { + // TODO a set would be better + private _components = new Map<string, FC>(); + + public eventBus = new EventTarget(); + + publicState(): PublicDeckyGlobalComponentsState { + return { components: this._components }; + } + + addComponent(path: string, component: FC) { + this._components.set(path, component); + this.notifyUpdate(); + } + + removeComponent(path: string) { + this._components.delete(path); + this.notifyUpdate(); + } + + private notifyUpdate() { + this.eventBus.dispatchEvent(new Event('update')); + } +} + +interface DeckyGlobalComponentsContext extends PublicDeckyGlobalComponentsState { + addComponent(path: string, component: FC): void; + removeComponent(path: string): void; +} + +const DeckyGlobalComponentsContext = createContext<DeckyGlobalComponentsContext>(null as any); + +export const useDeckyGlobalComponentsState = () => useContext(DeckyGlobalComponentsContext); + +interface Props { + deckyGlobalComponentsState: DeckyGlobalComponentsState; +} + +export const DeckyGlobalComponentsStateContextProvider: FC<Props> = ({ + children, + deckyGlobalComponentsState: deckyGlobalComponentsState, +}) => { + const [publicDeckyGlobalComponentsState, setPublicDeckyGlobalComponentsState] = + useState<PublicDeckyGlobalComponentsState>({ + ...deckyGlobalComponentsState.publicState(), + }); + + useEffect(() => { + function onUpdate() { + setPublicDeckyGlobalComponentsState({ ...deckyGlobalComponentsState.publicState() }); + } + + deckyGlobalComponentsState.eventBus.addEventListener('update', onUpdate); + + return () => deckyGlobalComponentsState.eventBus.removeEventListener('update', onUpdate); + }, []); + + const addComponent = deckyGlobalComponentsState.addComponent.bind(deckyGlobalComponentsState); + const removeComponent = deckyGlobalComponentsState.removeComponent.bind(deckyGlobalComponentsState); + + return ( + <DeckyGlobalComponentsContext.Provider + value={{ ...publicDeckyGlobalComponentsState, addComponent, removeComponent }} + > + {children} + </DeckyGlobalComponentsContext.Provider> + ); +}; diff --git a/frontend/src/components/DeckyToaster.tsx b/frontend/src/components/DeckyToaster.tsx new file mode 100644 index 00000000..eaee75eb --- /dev/null +++ b/frontend/src/components/DeckyToaster.tsx @@ -0,0 +1,54 @@ +import { ToastData, joinClassNames } from 'decky-frontend-lib'; +import { FC, useEffect, useState } from 'react'; +import { ReactElement } from 'react-markdown/lib/react-markdown'; + +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 sound + 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: NodeJS.Timer | 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 new file mode 100644 index 00000000..8732d7f8 --- /dev/null +++ b/frontend/src/components/DeckyToasterState.tsx @@ -0,0 +1,69 @@ +import { ToastData } from 'decky-frontend-lib'; +import { FC, createContext, useContext, useEffect, useState } from 'react'; + +interface PublicDeckyToasterState { + toasts: Set<ToastData>; +} + +export class DeckyToasterState { + // TODO a set would be better + 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; +} + +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/Markdown.tsx b/frontend/src/components/Markdown.tsx index 278e49cd..045b90a2 100644 --- a/frontend/src/components/Markdown.tsx +++ b/frontend/src/components/Markdown.tsx @@ -1,4 +1,4 @@ -import { Focusable } from 'decky-frontend-lib'; +import { Focusable, Router } from 'decky-frontend-lib'; import { FunctionComponent, useRef } from 'react'; import ReactMarkdown, { Options as ReactMarkdownOptions } from 'react-markdown'; import remarkGfm from 'remark-gfm'; @@ -21,8 +21,8 @@ const Markdown: FunctionComponent<MarkdownProps> = (props) => { <Focusable onActivate={() => {}} onOKButton={() => { - aRef?.current?.click(); props.onDismiss?.(); + Router.NavigateToExternalWeb(aRef.current!.href); }} style={{ display: 'inline' }} > diff --git a/frontend/src/components/QuickAccessVisibleState.tsx b/frontend/src/components/QuickAccessVisibleState.tsx index b5ee3b98..4df7e1a1 100644 --- a/frontend/src/components/QuickAccessVisibleState.tsx +++ b/frontend/src/components/QuickAccessVisibleState.tsx @@ -1,13 +1,27 @@ -import { FC, createContext, useContext } from 'react'; +import { FC, createContext, useContext, useEffect, useRef, useState } from 'react'; const QuickAccessVisibleState = createContext<boolean>(true); export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState); -interface Props { - visible: boolean; -} - -export const QuickAccessVisibleStateProvider: FC<Props> = ({ children, visible }) => { - return <QuickAccessVisibleState.Provider value={visible}>{children}</QuickAccessVisibleState.Provider>; +export const QuickAccessVisibleStateProvider: FC<{}> = ({ children }) => { + const divRef = useRef<HTMLDivElement>(null); + const [visible, setVisible] = useState<boolean>(false); + useEffect(() => { + const doc: Document | void | null = divRef?.current?.ownerDocument; + if (!doc) return; + setVisible(doc.visibilityState == 'visible'); + const onChange = (e: Event) => { + setVisible(doc.visibilityState == 'visible'); + }; + doc.addEventListener('visibilitychange', onChange); + return () => { + doc.removeEventListener('visibilitychange', onChange); + }; + }, [divRef]); + return ( + <div ref={divRef}> + <QuickAccessVisibleState.Provider value={visible}>{children}</QuickAccessVisibleState.Provider> + </div> + ); }; diff --git a/frontend/src/components/Toast.tsx b/frontend/src/components/Toast.tsx index 01a436d7..e7a220c2 100644 --- a/frontend/src/components/Toast.tsx +++ b/frontend/src/components/Toast.tsx @@ -2,13 +2,10 @@ import { ToastData, findModule, joinClassNames } from 'decky-frontend-lib'; import { FunctionComponent } from 'react'; interface ToastProps { - toast: { - data: ToastData; - nToastDurationMS: number; - }; + toast: ToastData; } -const toastClasses = findModule((mod) => { +export const toastClasses = findModule((mod) => { if (typeof mod !== 'object') return false; if (mod.ToastPlaceholder) { @@ -30,21 +27,19 @@ const templateClasses = findModule((mod) => { const Toast: FunctionComponent<ToastProps> = ({ toast }) => { return ( - <div - style={{ '--toast-duration': `${toast.nToastDurationMS}ms` } as React.CSSProperties} - className={toastClasses.toastEnter} - > + <div className={toastClasses.ToastPopup}> <div - onClick={toast.data.onClick} - className={joinClassNames(templateClasses.ShortTemplate, toast.data.className || '')} + style={{ '--toast-duration': `${toast.duration}ms` } as React.CSSProperties} + onClick={toast.onClick} + className={joinClassNames(templateClasses.ShortTemplate, toast.className || '')} > - {toast.data.logo && <div className={templateClasses.StandardLogoDimensions}>{toast.data.logo}</div>} - <div className={joinClassNames(templateClasses.Content, toast.data.contentClassName || '')}> + {toast.logo && <div className={templateClasses.StandardLogoDimensions}>{toast.logo}</div>} + <div className={joinClassNames(templateClasses.Content, toast.contentClassName || '')}> <div className={templateClasses.Header}> - {toast.data.icon && <div className={templateClasses.Icon}>{toast.data.icon}</div>} - <div className={templateClasses.Title}>{toast.data.title}</div> + {toast.icon && <div className={templateClasses.Icon}>{toast.icon}</div>} + <div className={templateClasses.Title}>{toast.title}</div> </div> - <div className={templateClasses.Body}>{toast.data.body}</div> + <div className={templateClasses.Body}>{toast.body}</div> </div> </div> </div> diff --git a/frontend/src/components/settings/pages/general/Updater.tsx b/frontend/src/components/settings/pages/general/Updater.tsx index b4ea8536..f617e0ff 100644 --- a/frontend/src/components/settings/pages/general/Updater.tsx +++ b/frontend/src/components/settings/pages/general/Updater.tsx @@ -14,6 +14,7 @@ import { useEffect, useState } from 'react'; import { FaArrowDown } from 'react-icons/fa'; import { VerInfo, callUpdaterMethod, finishUpdate } from '../../../../updater'; +import { findSP } from '../../../../utils/windows'; import { useDeckyState } from '../../../DeckyState'; import InlinePatchNotes from '../../../patchnotes/InlinePatchNotes'; import WithSuspense from '../../../WithSuspense'; @@ -21,6 +22,7 @@ import WithSuspense from '../../../WithSuspense'; const MarkdownRenderer = lazy(() => import('../../../Markdown')); function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | null; closeModal?: () => {} }) { + const SP = findSP(); return ( <Focusable onCancelButton={closeModal}> <FocusRing> @@ -50,12 +52,13 @@ function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | n )} fnGetId={(id) => id} nNumItems={versionInfo?.all?.length} - nHeight={window.innerHeight - 40} - nItemHeight={window.innerHeight - 40} + nHeight={SP.innerHeight - 40} + nItemHeight={SP.innerHeight - 40} nItemMarginX={0} initialColumn={0} autoFocus={true} - fnGetColumnWidth={() => window.innerWidth} + fnGetColumnWidth={() => SP.innerWidth} + name="Decky Updates" /> </FocusRing> </Focusable> |
