mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-17 00:37:49 +00:00
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
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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' }}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import {
|
||||
ReactRouter,
|
||||
Router,
|
||||
fakeRenderComponent,
|
||||
findInReactTree,
|
||||
findInTree,
|
||||
findModule,
|
||||
findModuleChild,
|
||||
gamepadDialogClasses,
|
||||
@@ -71,6 +74,11 @@ export async function startup() {
|
||||
window.DFL = {
|
||||
findModuleChild,
|
||||
findModule,
|
||||
ReactUtils: {
|
||||
fakeRenderComponent,
|
||||
findInReactTree,
|
||||
findInTree,
|
||||
},
|
||||
Router,
|
||||
ReactRouter,
|
||||
classes: {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
staticClasses,
|
||||
} from 'decky-frontend-lib';
|
||||
import { lazy } from 'react';
|
||||
import { FaPlug } from 'react-icons/fa';
|
||||
import { FaExclamationCircle, FaPlug } from 'react-icons/fa';
|
||||
|
||||
import { DeckyState, DeckyStateContextProvider, useDeckyState } from './components/DeckyState';
|
||||
import LegacyPlugin from './components/LegacyPlugin';
|
||||
@@ -41,7 +41,7 @@ class PluginLoader extends Logger {
|
||||
private tabsHook: TabsHook = new TabsHook();
|
||||
// private windowHook: WindowHook = new WindowHook();
|
||||
private routerHook: RouterHook = new RouterHook();
|
||||
public toaster: Toaster = new Toaster();
|
||||
public toaster: Toaster = new Toaster(this.routerHook);
|
||||
private deckyState: DeckyState = new DeckyState();
|
||||
|
||||
private reloadLock: boolean = false;
|
||||
@@ -233,13 +233,18 @@ class PluginLoader extends Logger {
|
||||
},
|
||||
});
|
||||
if (res.ok) {
|
||||
let plugin_export = await eval(await res.text());
|
||||
let plugin = plugin_export(this.createPluginAPI(name));
|
||||
this.plugins.push({
|
||||
...plugin,
|
||||
name: name,
|
||||
version: version,
|
||||
});
|
||||
try {
|
||||
let plugin_export = await eval(await res.text());
|
||||
let plugin = plugin_export(this.createPluginAPI(name));
|
||||
this.plugins.push({
|
||||
...plugin,
|
||||
name: name,
|
||||
version: version,
|
||||
});
|
||||
} catch (e) {
|
||||
this.error('Error loading plugin ' + name, e);
|
||||
this.toaster.toast({ title: 'Error loading ' + name, body: '' + e, icon: <FaExclamationCircle /> });
|
||||
}
|
||||
} else throw new Error(`${name} frontend_bundle not OK`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Patch, afterPatch, findModuleChild } from 'decky-frontend-lib';
|
||||
import { ReactElement, ReactNode, cloneElement, createElement, memo } from 'react';
|
||||
import { FC, ReactElement, ReactNode, cloneElement, createElement, memo } from 'react';
|
||||
import type { Route } from 'react-router';
|
||||
|
||||
import {
|
||||
DeckyGlobalComponentsState,
|
||||
DeckyGlobalComponentsStateContextProvider,
|
||||
useDeckyGlobalComponentsState,
|
||||
} from './components/DeckyGlobalComponentsState';
|
||||
import {
|
||||
DeckyRouterState,
|
||||
DeckyRouterStateContextProvider,
|
||||
@@ -22,8 +27,10 @@ class RouterHook extends Logger {
|
||||
private memoizedRouter: any;
|
||||
private gamepadWrapper: any;
|
||||
private routerState: DeckyRouterState = new DeckyRouterState();
|
||||
private globalComponentsState: DeckyGlobalComponentsState = new DeckyGlobalComponentsState();
|
||||
private wrapperPatch: Patch;
|
||||
private routerPatch?: Patch;
|
||||
public routes?: any[];
|
||||
|
||||
constructor() {
|
||||
super('RouterHook');
|
||||
@@ -42,24 +49,28 @@ class RouterHook extends Logger {
|
||||
|
||||
let Route: new () => Route;
|
||||
// Used to store the new replicated routes we create to allow routes to be unpatched.
|
||||
let toReplace = new Map<string, ReactNode>();
|
||||
const DeckyWrapper = ({ children }: { children: ReactElement }) => {
|
||||
const { routes, routePatches } = useDeckyRouterState();
|
||||
|
||||
const routeList = children.props.children[0].props.children;
|
||||
|
||||
const processList = (
|
||||
routeList: any[],
|
||||
routes: Map<string, RouterEntry> | null,
|
||||
routePatches: Map<string, Set<RoutePatch>>,
|
||||
save: boolean,
|
||||
) => {
|
||||
this.debug('Route list: ', routeList);
|
||||
if (save) this.routes = routeList;
|
||||
let routerIndex = routeList.length;
|
||||
if (!routeList[routerIndex - 1]?.length || routeList[routerIndex - 1]?.length !== routes.size) {
|
||||
if (routeList[routerIndex - 1]?.length && routeList[routerIndex - 1].length !== routes.size) routerIndex--;
|
||||
const newRouterArray: ReactElement[] = [];
|
||||
routes.forEach(({ component, props }, path) => {
|
||||
newRouterArray.push(
|
||||
<Route path={path} {...props}>
|
||||
{createElement(component)}
|
||||
</Route>,
|
||||
);
|
||||
});
|
||||
routeList[routerIndex] = newRouterArray;
|
||||
if (routes) {
|
||||
if (!routeList[routerIndex - 1]?.length || routeList[routerIndex - 1]?.length !== routes.size) {
|
||||
if (routeList[routerIndex - 1]?.length && routeList[routerIndex - 1].length !== routes.size) routerIndex--;
|
||||
const newRouterArray: ReactElement[] = [];
|
||||
routes.forEach(({ component, props }, path) => {
|
||||
newRouterArray.push(
|
||||
<Route path={path} {...props}>
|
||||
{createElement(component)}
|
||||
</Route>,
|
||||
);
|
||||
});
|
||||
routeList[routerIndex] = newRouterArray;
|
||||
}
|
||||
}
|
||||
routeList.forEach((route: Route, index: number) => {
|
||||
const replaced = toReplace.get(route?.props?.path as string);
|
||||
@@ -85,19 +96,40 @@ class RouterHook extends Logger {
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
let toReplace = new Map<string, ReactNode>();
|
||||
const DeckyWrapper = ({ children }: { children: ReactElement }) => {
|
||||
const { routes, routePatches } = useDeckyRouterState();
|
||||
const mainRouteList = children.props.children[0].props.children;
|
||||
const ingameRouteList = children.props.children[1].props.children; // /appoverlay and /apprunning
|
||||
processList(mainRouteList, routes, routePatches, true);
|
||||
processList(ingameRouteList, null, routePatches, false);
|
||||
|
||||
this.debug('Rerendered routes list');
|
||||
return children;
|
||||
};
|
||||
|
||||
let renderedComponents: ReactElement[] = [];
|
||||
|
||||
const DeckyGlobalComponentsWrapper = () => {
|
||||
const { components } = useDeckyGlobalComponentsState();
|
||||
if (renderedComponents.length != components.size) {
|
||||
this.debug('Rerendering global components');
|
||||
renderedComponents = Array.from(components.values()).map((GComponent) => <GComponent />);
|
||||
}
|
||||
return <>{renderedComponents}</>;
|
||||
};
|
||||
|
||||
this.wrapperPatch = afterPatch(this.gamepadWrapper, 'render', (_: any, ret: any) => {
|
||||
if (ret?.props?.children?.props?.children?.length == 5) {
|
||||
if (ret?.props?.children?.props?.children?.length == 5 || ret?.props?.children?.props?.children?.length == 4) {
|
||||
const idx = ret?.props?.children?.props?.children?.length == 4 ? 1 : 2;
|
||||
if (
|
||||
ret.props.children.props.children[2]?.props?.children?.[0]?.type?.type
|
||||
ret.props.children.props.children[idx]?.props?.children?.[0]?.type?.type
|
||||
?.toString()
|
||||
?.includes('GamepadUI.Settings.Root()')
|
||||
) {
|
||||
if (!this.router) {
|
||||
this.router = ret.props.children.props.children[2]?.props?.children?.[0]?.type;
|
||||
this.router = ret.props.children.props.children[idx]?.props?.children?.[0]?.type;
|
||||
this.routerPatch = afterPatch(this.router, 'type', (_: any, ret: any) => {
|
||||
if (!Route)
|
||||
Route = ret.props.children[0].props.children.find((x: any) => x.props.path == '/createaccount').type;
|
||||
@@ -111,7 +143,12 @@ class RouterHook extends Logger {
|
||||
this.memoizedRouter = memo(this.router.type);
|
||||
this.memoizedRouter.isDeckyRouter = true;
|
||||
}
|
||||
ret.props.children.props.children[2].props.children[0].type = this.memoizedRouter;
|
||||
ret.props.children.props.children.push(
|
||||
<DeckyGlobalComponentsStateContextProvider deckyGlobalComponentsState={this.globalComponentsState}>
|
||||
<DeckyGlobalComponentsWrapper />
|
||||
</DeckyGlobalComponentsStateContextProvider>,
|
||||
);
|
||||
ret.props.children.props.children[idx].props.children[0].type = this.memoizedRouter;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
@@ -126,6 +163,14 @@ class RouterHook extends Logger {
|
||||
return this.routerState.addPatch(path, patch);
|
||||
}
|
||||
|
||||
addGlobalComponent(name: string, component: FC) {
|
||||
this.globalComponentsState.addComponent(name, component);
|
||||
}
|
||||
|
||||
removeGlobalComponent(name: string) {
|
||||
this.globalComponentsState.removeComponent(name);
|
||||
}
|
||||
|
||||
removePatch(path: string, patch: RoutePatch) {
|
||||
this.routerState.removePatch(path, patch);
|
||||
}
|
||||
|
||||
+57
-86
@@ -1,5 +1,4 @@
|
||||
import { Patch, QuickAccessTab, afterPatch, sleep } from 'decky-frontend-lib';
|
||||
import { memo } from 'react';
|
||||
import { QuickAccessTab, quickAccessMenuClasses, sleep } from 'decky-frontend-lib';
|
||||
|
||||
import { QuickAccessVisibleStateProvider } from './components/QuickAccessVisibleState';
|
||||
import Logger from './logger';
|
||||
@@ -28,15 +27,7 @@ interface Tab {
|
||||
class TabsHook extends Logger {
|
||||
// private keys = 7;
|
||||
tabs: Tab[] = [];
|
||||
private quickAccess: any;
|
||||
private tabRenderer: any;
|
||||
private memoizedQuickAccess: any;
|
||||
private cNode: any;
|
||||
|
||||
private qAPTree: any;
|
||||
private rendererTree: any;
|
||||
|
||||
private cNodePatch?: Patch;
|
||||
private oFilter: (...args: any[]) => any;
|
||||
|
||||
constructor() {
|
||||
super('TabsHook');
|
||||
@@ -46,84 +37,63 @@ class TabsHook extends Logger {
|
||||
window.__TABS_HOOK_INSTANCE = this;
|
||||
|
||||
const self = this;
|
||||
const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current;
|
||||
let scrollRoot: any;
|
||||
async function findScrollRoot(currentNode: any, iters: number): Promise<any> {
|
||||
if (iters >= 30) {
|
||||
self.error(
|
||||
'Scroll root was not found before hitting the recursion limit, a developer will need to increase the limit.',
|
||||
);
|
||||
return null;
|
||||
const oFilter = (this.oFilter = Array.prototype.filter);
|
||||
Array.prototype.filter = function patchedFilter(...args: any[]) {
|
||||
if (isTabsArray(this)) {
|
||||
self.render(this);
|
||||
}
|
||||
currentNode = currentNode?.child;
|
||||
if (currentNode?.type?.prototype?.RemoveSmartScrollContainer) {
|
||||
self.log(`Scroll root was found in ${iters} recursion cycles`);
|
||||
return currentNode;
|
||||
}
|
||||
if (!currentNode) return null;
|
||||
if (currentNode.sibling) {
|
||||
let node = await findScrollRoot(currentNode.sibling, iters + 1);
|
||||
if (node !== null) return node;
|
||||
}
|
||||
return await findScrollRoot(currentNode, iters + 1);
|
||||
}
|
||||
(async () => {
|
||||
scrollRoot = await findScrollRoot(tree, 0);
|
||||
while (!scrollRoot) {
|
||||
this.log('Failed to find scroll root node, reattempting in 5 seconds');
|
||||
await sleep(5000);
|
||||
scrollRoot = await findScrollRoot(tree, 0);
|
||||
}
|
||||
let newQA: any;
|
||||
let newQATabRenderer: any;
|
||||
this.cNodePatch = afterPatch(scrollRoot.stateNode, 'render', (_: any, ret: any) => {
|
||||
if (!this.quickAccess && ret.props.children.props.children[4]) {
|
||||
this.quickAccess = ret?.props?.children?.props?.children[4].type;
|
||||
newQA = (...args: any) => {
|
||||
const ret = this.quickAccess.type(...args);
|
||||
if (ret) {
|
||||
if (!newQATabRenderer) {
|
||||
this.tabRenderer = ret.props.children[1].children.type;
|
||||
newQATabRenderer = (...qamArgs: any[]) => {
|
||||
const oFilter = Array.prototype.filter;
|
||||
Array.prototype.filter = function (...args: any[]) {
|
||||
if (isTabsArray(this)) {
|
||||
self.render(this, qamArgs[0].visible);
|
||||
}
|
||||
// @ts-ignore
|
||||
return oFilter.call(this, ...args);
|
||||
};
|
||||
// TODO remove array hack entirely and use this instead const tabs = ret.props.children.props.children[0].props.children[1].props.children[0].props.children[0].props.tabs
|
||||
const ret = this.tabRenderer(...qamArgs);
|
||||
Array.prototype.filter = oFilter;
|
||||
return ret;
|
||||
};
|
||||
}
|
||||
this.rendererTree = ret.props.children[1].children;
|
||||
ret.props.children[1].children.type = newQATabRenderer;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
this.memoizedQuickAccess = memo(newQA);
|
||||
this.memoizedQuickAccess.isDeckyQuickAccess = true;
|
||||
// @ts-ignore
|
||||
return oFilter.call(this, ...args);
|
||||
};
|
||||
|
||||
if (document.title != 'SP')
|
||||
try {
|
||||
const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current;
|
||||
let qAMRoot: any;
|
||||
async function findQAMRoot(currentNode: any, iters: number): Promise<any> {
|
||||
if (iters >= 60) {
|
||||
// currently 44
|
||||
return null;
|
||||
}
|
||||
currentNode = currentNode?.child;
|
||||
if (
|
||||
currentNode?.memoizedProps?.className &&
|
||||
currentNode?.memoizedProps?.className.startsWith(quickAccessMenuClasses.ViewPlaceholder)
|
||||
) {
|
||||
self.log(`QAM root was found in ${iters} recursion cycles`);
|
||||
return currentNode;
|
||||
}
|
||||
if (!currentNode) return null;
|
||||
if (currentNode.sibling) {
|
||||
let node = await findQAMRoot(currentNode.sibling, iters + 1);
|
||||
if (node !== null) return node;
|
||||
}
|
||||
return await findQAMRoot(currentNode, iters + 1);
|
||||
}
|
||||
if (ret.props.children.props.children[4]) {
|
||||
this.qAPTree = ret.props.children.props.children[4];
|
||||
ret.props.children.props.children[4].type = this.memoizedQuickAccess;
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
this.cNode = scrollRoot;
|
||||
this.cNode.stateNode.forceUpdate();
|
||||
this.log('Finished initial injection');
|
||||
})();
|
||||
(async () => {
|
||||
qAMRoot = await findQAMRoot(tree, 0);
|
||||
while (!qAMRoot) {
|
||||
this.error(
|
||||
'Failed to find QAM root node, reattempting in 5 seconds. A developer may need to increase the recursion limit.',
|
||||
);
|
||||
await sleep(5000);
|
||||
qAMRoot = await findQAMRoot(tree, 0);
|
||||
}
|
||||
|
||||
while (!qAMRoot?.stateNode?.forceUpdate) {
|
||||
qAMRoot = qAMRoot.return;
|
||||
}
|
||||
qAMRoot.stateNode.shouldComponentUpdate = () => true;
|
||||
qAMRoot.stateNode.forceUpdate();
|
||||
delete qAMRoot.stateNode.shouldComponentUpdate;
|
||||
})();
|
||||
} catch (e) {
|
||||
this.log('Failed to rerender QAM', e);
|
||||
}
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.cNodePatch?.unpatch();
|
||||
if (this.qAPTree) this.qAPTree.type = this.quickAccess;
|
||||
if (this.rendererTree) this.rendererTree.type = this.tabRenderer;
|
||||
if (this.cNode) this.cNode.stateNode.forceUpdate();
|
||||
Array.prototype.filter = this.oFilter;
|
||||
}
|
||||
|
||||
add(tab: Tab) {
|
||||
@@ -136,13 +106,14 @@ class TabsHook extends Logger {
|
||||
this.tabs = this.tabs.filter((tab) => tab.id !== id);
|
||||
}
|
||||
|
||||
render(existingTabs: any[], visible: boolean) {
|
||||
render(existingTabs: any[]) {
|
||||
for (const { title, icon, content, id } of this.tabs) {
|
||||
existingTabs.push({
|
||||
key: id,
|
||||
title,
|
||||
tab: icon,
|
||||
panel: <QuickAccessVisibleStateProvider visible={visible}>{content}</QuickAccessVisibleStateProvider>,
|
||||
decky: true,
|
||||
panel: <QuickAccessVisibleStateProvider>{content}</QuickAccessVisibleStateProvider>,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+132
-79
@@ -1,8 +1,10 @@
|
||||
import { Patch, ToastData, afterPatch, findInReactTree, findModuleChild, sleep } from 'decky-frontend-lib';
|
||||
import { ReactNode } from 'react';
|
||||
import { Patch, ToastData, sleep } from 'decky-frontend-lib';
|
||||
|
||||
import DeckyToaster from './components/DeckyToaster';
|
||||
import { DeckyToasterState, DeckyToasterStateContextProvider } from './components/DeckyToasterState';
|
||||
import Toast from './components/Toast';
|
||||
import Logger from './logger';
|
||||
import RouterHook from './router-hook';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -13,12 +15,15 @@ declare global {
|
||||
|
||||
class Toaster extends Logger {
|
||||
private instanceRetPatch?: Patch;
|
||||
private routerHook: RouterHook;
|
||||
private toasterState: DeckyToasterState = new DeckyToasterState();
|
||||
private node: any;
|
||||
private settingsModule: any;
|
||||
private ready: boolean = false;
|
||||
|
||||
constructor() {
|
||||
constructor(routerHook: RouterHook) {
|
||||
super('Toaster');
|
||||
this.routerHook = routerHook;
|
||||
|
||||
window.__TOASTER_INSTANCE?.deinit?.();
|
||||
window.__TOASTER_INSTANCE = this;
|
||||
@@ -26,87 +31,135 @@ class Toaster extends Logger {
|
||||
}
|
||||
|
||||
async init() {
|
||||
let instance: any;
|
||||
|
||||
while (true) {
|
||||
instance = findInReactTree(
|
||||
(document.getElementById('root') as any)._reactRootContainer._internalRoot.current,
|
||||
(x) => x?.memoizedProps?.className?.startsWith?.('toastmanager_ToastPlaceholder'),
|
||||
);
|
||||
if (instance) break;
|
||||
this.debug('finding instance');
|
||||
await sleep(2000);
|
||||
}
|
||||
|
||||
this.node = instance.return.return;
|
||||
let toast: any;
|
||||
let renderedToast: ReactNode = null;
|
||||
this.node.stateNode.render = (...args: any[]) => {
|
||||
const ret = this.node.stateNode.__proto__.render.call(this.node.stateNode, ...args);
|
||||
if (ret) {
|
||||
this.instanceRetPatch = afterPatch(ret, 'type', (_: any, ret: any) => {
|
||||
if (ret?.props?.children[1]?.children?.props) {
|
||||
const currentToast = ret.props.children[1].children.props.notification;
|
||||
if (currentToast?.decky) {
|
||||
if (currentToast == toast) {
|
||||
ret.props.children[1].children = renderedToast;
|
||||
} else {
|
||||
toast = currentToast;
|
||||
renderedToast = <Toast toast={toast} />;
|
||||
ret.props.children[1].children = renderedToast;
|
||||
}
|
||||
} else {
|
||||
toast = null;
|
||||
renderedToast = null;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
this.node.stateNode.shouldComponentUpdate = () => {
|
||||
return false;
|
||||
};
|
||||
delete this.node.stateNode.render;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
this.settingsModule = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (typeof m[prop]?.settings && m[prop]?.communityPreferences) return m[prop];
|
||||
}
|
||||
});
|
||||
this.log('Initialized');
|
||||
this.ready = true;
|
||||
this.routerHook.addGlobalComponent('DeckyToaster', () => (
|
||||
<DeckyToasterStateContextProvider deckyToasterState={this.toasterState}>
|
||||
<DeckyToaster />
|
||||
</DeckyToasterStateContextProvider>
|
||||
));
|
||||
// let instance: any;
|
||||
// while (true) {
|
||||
// instance = findInReactTree(
|
||||
// (document.getElementById('root') as any)._reactRootContainer._internalRoot.current,
|
||||
// (x) => x?.memoizedProps?.className?.startsWith?.('toastmanager_ToastPlaceholder'),
|
||||
// );
|
||||
// if (instance) break;
|
||||
// this.debug('finding instance');
|
||||
// await sleep(2000);
|
||||
// }
|
||||
// // const windowManager = findModuleChild((m) => {
|
||||
// // if (typeof m !== 'object') return false;
|
||||
// // for (let prop in m) {
|
||||
// // if (m[prop]?.prototype?.GetRenderElement) return m[prop];
|
||||
// // }
|
||||
// // return false;
|
||||
// // });
|
||||
// this.node = instance.return.return;
|
||||
// let toast: any;
|
||||
// let renderedToast: ReactNode = null;
|
||||
// console.log(instance, this.node);
|
||||
// // replacePatch(window.SteamClient.BrowserView, "Destroy", (args: any[]) => {
|
||||
// // console.debug("destroy", args)
|
||||
// // return callOriginal;
|
||||
// // })
|
||||
// // let node = this.node.child.updateQueue.lastEffect;
|
||||
// // while (node.next && !node.deckyPatched) {
|
||||
// // node = node.next;
|
||||
// // if (node.deps[1] == "notificationtoasts") {
|
||||
// // console.log("Deleting destroy");
|
||||
// // node.deckyPatched = true;
|
||||
// // node.create = () => {console.debug("VVVVVVVVVVV")};
|
||||
// // node.destroy = () => {console.debug("AAAAAAAAAAAAAAAAaaaaaaaaaaaaaaa")};
|
||||
// // }
|
||||
// // }
|
||||
// this.node.stateNode.render = (...args: any[]) => {
|
||||
// const ret = this.node.stateNode.__proto__.render.call(this.node.stateNode, ...args);
|
||||
// console.log('toast', ret);
|
||||
// if (ret) {
|
||||
// console.log(ret)
|
||||
// // this.instanceRetPatch = replacePatch(ret, 'type', (innerArgs: any) => {
|
||||
// // console.log("inner toast", innerArgs)
|
||||
// // // @ts-ignore
|
||||
// // const oldEffect = window.SP_REACT.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current.useEffect;
|
||||
// // // @ts-ignore
|
||||
// // window.SP_REACT.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current.useEffect = (effect, deps) => {
|
||||
// // console.log(effect, deps)
|
||||
// // if (deps?.[1] == "notificationtoasts") {
|
||||
// // console.log("run")
|
||||
// // effect();
|
||||
// // }
|
||||
// // return oldEffect(effect, deps);
|
||||
// // }
|
||||
// // const ret = this.instanceRetPatch?.original(...args);
|
||||
// // console.log("inner ret", ret)
|
||||
// // // @ts-ignore
|
||||
// // window.SP_REACT.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current.useEffect = oldEffect;
|
||||
// // return ret
|
||||
// // });
|
||||
// }
|
||||
// // console.log("toast ret", ret)
|
||||
// // if (ret?.props?.children[1]?.children?.props) {
|
||||
// // const currentToast = ret.props.children[1].children.props.notification;
|
||||
// // if (currentToast?.decky) {
|
||||
// // if (currentToast == toast) {
|
||||
// // ret.props.children[1].children = renderedToast;
|
||||
// // } else {
|
||||
// // toast = currentToast;
|
||||
// // renderedToast = <Toast toast={toast} />;
|
||||
// // ret.props.children[1].children = renderedToast;
|
||||
// // }
|
||||
// // } else {
|
||||
// // toast = null;
|
||||
// // renderedToast = null;
|
||||
// // }
|
||||
// // }
|
||||
// // return ret;
|
||||
// // });
|
||||
// // }
|
||||
// return ret;
|
||||
// };
|
||||
// this.settingsModule = findModuleChild((m) => {
|
||||
// if (typeof m !== 'object') return undefined;
|
||||
// for (let prop in m) {
|
||||
// if (typeof m[prop]?.settings && m[prop]?.communityPreferences) return m[prop];
|
||||
// }
|
||||
// });
|
||||
// // const idx = FocusNavController.m_ActiveContext.m_rgGamepadNavigationTrees.findIndex((x: any) => x.m_ID == "ToastContainer");
|
||||
// // if (idx > -1) {
|
||||
// // FocusNavController.m_ActiveContext.m_rgGamepadNavigationTrees.splice(idx, 1)
|
||||
// // }
|
||||
// this.node.stateNode.forceUpdate();
|
||||
// this.node.stateNode.shouldComponentUpdate = () => {
|
||||
// return false;
|
||||
// };
|
||||
// this.log('Initialized');
|
||||
// this.ready = true;
|
||||
}
|
||||
|
||||
async toast(toast: ToastData) {
|
||||
while (!this.ready) {
|
||||
await sleep(100);
|
||||
}
|
||||
const settings = this.settingsModule?.settings;
|
||||
let toastData = {
|
||||
nNotificationID: window.NotificationStore.m_nNextTestNotificationID++,
|
||||
rtCreated: Date.now(),
|
||||
eType: 15,
|
||||
nToastDurationMS: toast.duration || 5e3,
|
||||
data: toast,
|
||||
decky: true,
|
||||
};
|
||||
// @ts-ignore
|
||||
toastData.data.appid = () => 0;
|
||||
if (
|
||||
(settings?.bDisableAllToasts && !toast.critical) ||
|
||||
(settings?.bDisableToastsInGame && !toast.critical && window.NotificationStore.BIsUserInGame())
|
||||
)
|
||||
return;
|
||||
window.NotificationStore.m_rgNotificationToasts.push(toastData);
|
||||
window.NotificationStore.DispatchNextToast();
|
||||
toast(toast: ToastData) {
|
||||
toast.duration = toast.duration || 5e3;
|
||||
this.toasterState.addToast(toast);
|
||||
// const settings = this.settingsModule?.settings;
|
||||
// let toastData = {
|
||||
// nNotificationID: window.NotificationStore.m_nNextTestNotificationID++,
|
||||
// rtCreated: Date.now(),
|
||||
// eType: 15,
|
||||
// nToastDurationMS: toast.duration || 5e3,
|
||||
// data: toast,
|
||||
// decky: true,
|
||||
// };
|
||||
// // @ts-ignore
|
||||
// toastData.data.appid = () => 0;
|
||||
// if (
|
||||
// (settings?.bDisableAllToasts && !toast.critical) ||
|
||||
// (settings?.bDisableToastsInGame && !toast.critical && window.NotificationStore.BIsUserInGame())
|
||||
// )
|
||||
// return;
|
||||
// window.NotificationStore.m_rgNotificationToasts.push(toastData);
|
||||
// window.NotificationStore.DispatchNextToast();
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.instanceRetPatch?.unpatch();
|
||||
this.node && delete this.node.stateNode.shouldComponentUpdate;
|
||||
this.node && this.node.stateNode.forceUpdate();
|
||||
this.routerHook.removeGlobalComponent('DeckyToaster');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export function findSP(): Window {
|
||||
// old (SP as host)
|
||||
if (document.title == 'SP') return window;
|
||||
// new (SP as popup)
|
||||
return FocusNavController.m_ActiveContext.m_rgGamepadNavigationTrees.find((x: any) => x.m_ID == 'root_1_').Root
|
||||
.Element.ownerDocument.defaultView;
|
||||
}
|
||||
Reference in New Issue
Block a user