summaryrefslogtreecommitdiff
path: root/frontend/src/components
diff options
context:
space:
mode:
authorAAGaming <aa@mail.catvibers.me>2022-10-24 19:14:56 -0400
committerGitHub <noreply@github.com>2022-10-24 16:14:56 -0700
commit84c3b039c385ad872bb0f22eba7a3d2cd4a5ea10 (patch)
tree20b13066c6256cc6ca1beac085094c7964226a37 /frontend/src/components
parent2e6b3834da357c7e81821ce60bad36f54dd9fa6e (diff)
downloaddecky-loader-84c3b039c385ad872bb0f22eba7a3d2cd4a5ea10.tar.gz
decky-loader-84c3b039c385ad872bb0f22eba7a3d2cd4a5ea10.zip
preview 10/21/2022 fixes (#234)
* initial fixes: everything working except toasts and patch notes * tabshook changes, disable toaster for now * prettier * oops * implement custom toaster because I am tired of Valve's shit also fix QAM not injecting sometimes * remove extra logging * add findSP, fix patch notes, fix vscode screwup * fix patch notes * show error when plugin frontends fail to load * add get_tab_lambda * add css and has_element helpers to Tab * small modals fixup * Don't forceUpdate QuickAccess on stable * add routes prop used to get tabs component * add more dev utils to DFL global
Diffstat (limited to 'frontend/src/components')
-rw-r--r--frontend/src/components/DeckyGlobalComponentsState.tsx74
-rw-r--r--frontend/src/components/DeckyToaster.tsx54
-rw-r--r--frontend/src/components/DeckyToasterState.tsx69
-rw-r--r--frontend/src/components/Markdown.tsx4
-rw-r--r--frontend/src/components/QuickAccessVisibleState.tsx28
-rw-r--r--frontend/src/components/Toast.tsx27
-rw-r--r--frontend/src/components/settings/pages/general/Updater.tsx9
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>