mirror of
https://github.com/SteamDeckHomebrew/decky-frontend-lib.git
synced 2026-05-27 21:26:57 +02:00
feat(*): add v4 webpack api and port everything to it
also restructures a bunch
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@decky/frontend",
|
||||
"name": "@decky/ui",
|
||||
"version": "4.0.0",
|
||||
"description": "A library for interacting with the Steam frontend in Decky plugins and elsewhere.",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@@ -8,9 +8,8 @@ export interface ButtonItemProps extends ItemProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
export const ButtonItem =
|
||||
(CommonUIModule.ButtonField ||
|
||||
Object.values(CommonUIModule).find(
|
||||
(mod: any) =>
|
||||
mod?.render?.toString()?.includes('"highlightOnFocus","childrenContainerWidth"') ||
|
||||
mod?.render?.toString()?.includes('childrenContainerWidth:"min"'),
|
||||
)) as FC<ButtonItemProps>;
|
||||
) as FC<ButtonItemProps>;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { HTMLAttributes, ReactNode, RefAttributes, VFC } from 'react';
|
||||
|
||||
import { findModuleChild } from '../webpack';
|
||||
import { Export, findModuleExport } from '../webpack';
|
||||
|
||||
export interface CarouselProps extends HTMLAttributes<HTMLDivElement> {
|
||||
autoFocus?: boolean;
|
||||
@@ -20,9 +20,4 @@ export interface CarouselProps extends HTMLAttributes<HTMLDivElement> {
|
||||
scrollToAlignment?: 'center';
|
||||
}
|
||||
|
||||
export const Carousel = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.render?.toString().includes('setFocusedColumn:')) return m[prop];
|
||||
}
|
||||
}) as VFC<CarouselProps & RefAttributes<HTMLDivElement>>;
|
||||
export const Carousel = findModuleExport((e: Export) => e.render?.toString().includes('setFocusedColumn:')) as VFC<CarouselProps & RefAttributes<HTMLDivElement>>;
|
||||
9
src/components/ControlsList.tsx
Normal file
9
src/components/ControlsList.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Export, findModuleExport } from '../webpack';
|
||||
import { FC } from 'react';
|
||||
|
||||
export interface ControlsListProps {
|
||||
alignItems?: 'left' | 'right' | 'center';
|
||||
spacing?: 'standard' | 'extra';
|
||||
}
|
||||
|
||||
export const ControlsList: FC<ControlsListProps> = findModuleExport((e: Export) => e?.toString && e.toString().includes('().ControlsListChild') && e.toString().includes('().ControlsListOuterPanel'));
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FC, ReactNode, RefAttributes } from 'react';
|
||||
|
||||
import { findModuleChild } from '../webpack';
|
||||
import { Export, findModuleExport } from '../webpack';
|
||||
import { FooterLegendProps } from './FooterLegend';
|
||||
|
||||
export interface FieldProps extends FooterLegendProps {
|
||||
@@ -23,9 +23,4 @@ export interface FieldProps extends FooterLegendProps {
|
||||
onClick?: (e: CustomEvent | MouseEvent) => void;
|
||||
}
|
||||
|
||||
export const Field = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.render?.toString().includes('"shift-children-below"')) return m[prop];
|
||||
}
|
||||
}) as FC<FieldProps & RefAttributes<HTMLDivElement>>;
|
||||
export const Field = findModuleExport((e: Export) => e?.render?.toString().includes('"shift-children-below"')) as FC<FieldProps & RefAttributes<HTMLDivElement>>;
|
||||
13
src/components/FocusRing.ts
Normal file
13
src/components/FocusRing.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ElementType, FC, ReactNode } from 'react';
|
||||
|
||||
import { Export, findModuleExport } from '../webpack';
|
||||
|
||||
export interface FocusRingProps {
|
||||
className?: string;
|
||||
rootClassName?: string;
|
||||
render?: ElementType;
|
||||
children?: ReactNode;
|
||||
NavigationManager?: any;
|
||||
}
|
||||
|
||||
export const FocusRing = findModuleExport((e: Export) => e?.toString()?.includes('.GetShowDebugFocusRing())')) as FC<FocusRingProps>;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { HTMLAttributes, ReactNode, RefAttributes, VFC } from 'react';
|
||||
|
||||
import { findModuleChild } from '../webpack';
|
||||
import { Export, findModuleExport } from '../webpack';
|
||||
import { FooterLegendProps } from './FooterLegend';
|
||||
|
||||
export interface FocusableProps extends HTMLAttributes<HTMLDivElement>, FooterLegendProps {
|
||||
@@ -13,10 +13,4 @@ export interface FocusableProps extends HTMLAttributes<HTMLDivElement>, FooterLe
|
||||
onCancel?: (e: CustomEvent) => void;
|
||||
}
|
||||
|
||||
export const Focusable = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.render?.toString()?.includes('["flow-children","onActivate","onCancel","focusClassName",'))
|
||||
return m[prop];
|
||||
}
|
||||
}) as VFC<FocusableProps & RefAttributes<HTMLDivElement>>;
|
||||
export const Focusable = findModuleExport((e: Export) => e?.render?.toString()?.includes('["flow-children","onActivate","onCancel","focusClassName",')) as VFC<FocusableProps & RefAttributes<HTMLDivElement>>;
|
||||
18
src/components/Marquee.tsx
Normal file
18
src/components/Marquee.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { CSSProperties, FC } from 'react';
|
||||
|
||||
import { Export, findModuleExport } from '../webpack';
|
||||
|
||||
export interface MarqueeProps {
|
||||
play?: boolean;
|
||||
direction?: 'left' | 'right';
|
||||
speed?: number;
|
||||
delay?: number;
|
||||
fadeLength?: number;
|
||||
center?: boolean;
|
||||
resetOnPause?: boolean;
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Marquee: FC<MarqueeProps> = findModuleExport((e: Export) => e?.toString && e.toString().includes('.Marquee') && e.toString().includes('--fade-length'));
|
||||
57
src/components/Menu.tsx
Executable file
57
src/components/Menu.tsx
Executable file
@@ -0,0 +1,57 @@
|
||||
import { FC, ReactNode } from 'react';
|
||||
|
||||
import { fakeRenderComponent } from '../utils';
|
||||
import { Export, findModuleExport } from '../webpack';
|
||||
import { FooterLegendProps } from './FooterLegend';
|
||||
|
||||
export const showContextMenu: (children: ReactNode, parent?: EventTarget) => void = findModuleExport(
|
||||
(e: Export) => typeof e === 'function' && e.toString().includes('stopPropagation))'),
|
||||
);
|
||||
|
||||
export interface MenuProps extends FooterLegendProps {
|
||||
label: string;
|
||||
onCancel?(): void;
|
||||
cancelText?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const Menu: FC<MenuProps> = findModuleExport(
|
||||
(e: Export) => e?.prototype?.HideIfSubmenu && e?.prototype?.HideMenu,
|
||||
);
|
||||
|
||||
export interface MenuGroupProps {
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const MenuGroup: FC<MenuGroupProps> = findModuleExport(
|
||||
(e: Export) =>
|
||||
(e?.toString()?.includes?.('bInGamepadUI:') &&
|
||||
fakeRenderComponent(() => e({ overview: { appid: 7 } }))?.type?.prototype?.RenderSubMenu) ||
|
||||
(e?.prototype?.RenderSubMenu && e?.prototype?.ShowSubMenu),
|
||||
);
|
||||
export interface MenuItemProps extends FooterLegendProps {
|
||||
bInteractableItem?: boolean;
|
||||
onClick?(evt: Event): void;
|
||||
onSelected?(evt: Event): void;
|
||||
onMouseEnter?(evt: MouseEvent): void;
|
||||
onMoveRight?(): void;
|
||||
selected?: boolean;
|
||||
disabled?: boolean;
|
||||
bPlayAudio?: boolean;
|
||||
tone?: 'positive' | 'emphasis' | 'destructive';
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const MenuItem: FC<MenuItemProps> = findModuleExport(
|
||||
(e: Export) =>
|
||||
e?.render?.toString()?.includes('bPlayAudio:') || (e?.prototype?.OnOKButton && e?.prototype?.OnMouseEnter),
|
||||
);
|
||||
|
||||
/*
|
||||
all().map(m => {
|
||||
if (typeof m !== "object") return undefined;
|
||||
for (let prop in m) { if (m[prop]?.prototype?.OK && m[prop]?.prototype?.Cancel && m[prop]?.prototype?.render) return m[prop]}
|
||||
}).find(x => x)
|
||||
*/
|
||||
119
src/components/Modal.tsx
Executable file
119
src/components/Modal.tsx
Executable file
@@ -0,0 +1,119 @@
|
||||
import { FC, ReactNode } from 'react';
|
||||
|
||||
import { findSP } from '../utils';
|
||||
import { Export, findModule, findModuleByExport, findModuleExport } from '../webpack';
|
||||
|
||||
// All of the popout options + strTitle are related. Proper usage is not yet known...
|
||||
export interface ShowModalProps {
|
||||
browserContext?: unknown;
|
||||
bForcePopOut?: boolean;
|
||||
bHideActionIcons?: boolean;
|
||||
bHideMainWindowForPopouts?: boolean;
|
||||
bNeverPopOut?: boolean;
|
||||
fnOnClose?: () => void; // Seems to be the same as "closeModal" callback, but only when the modal is a popout. Will no longer work after "Update" invocation!
|
||||
popupHeight?: number;
|
||||
popupWidth?: number;
|
||||
promiseRenderComplete?: Promise<void>; // Invoked once the render is complete. Currently, it seems to be used as image loading success/error callback...
|
||||
strTitle?: string;
|
||||
}
|
||||
|
||||
export interface ShowModalResult {
|
||||
// This method will not invoke any of the variations of "closeModal" callbacks!
|
||||
Close: () => void;
|
||||
|
||||
// This method will replace the modal element completely and will not update the callback chains,
|
||||
// meaning that "closeModal" and etc. will not automatically close the modal anymore (also "fnOnClose"
|
||||
// will not be even called upon close anymore)! You have to manually call the "Close" method when, for example,
|
||||
// the "closeModal" is invoked in the newly updated modal:
|
||||
// <ModalRoot closeModal={() => { console.log("ABOUT TO CLOSE"); showModalRes.Close(); }} />
|
||||
Update: (modal: ReactNode) => void;
|
||||
}
|
||||
|
||||
const showModalRaw: (
|
||||
modal: ReactNode,
|
||||
parent?: EventTarget,
|
||||
title?: string,
|
||||
props?: ShowModalProps,
|
||||
unknown1?: unknown,
|
||||
hideActions?: { bHideActions?: boolean },
|
||||
modalManager?: unknown,
|
||||
) => ShowModalResult = findModuleExport(
|
||||
(e: Export) =>
|
||||
typeof e === 'function' && e.toString().includes('props.bDisableBackgroundDismiss') && !e?.prototype?.Cancel,
|
||||
);
|
||||
|
||||
export const showModal = (
|
||||
modal: ReactNode,
|
||||
parent?: EventTarget,
|
||||
props: ShowModalProps = {
|
||||
strTitle: 'Decky Dialog',
|
||||
bHideMainWindowForPopouts: false,
|
||||
},
|
||||
): ShowModalResult => {
|
||||
return showModalRaw(modal, parent || findSP(), props.strTitle, props, undefined, {
|
||||
bHideActions: props.bHideActionIcons,
|
||||
});
|
||||
};
|
||||
|
||||
export interface ModalRootProps {
|
||||
children?: ReactNode;
|
||||
onCancel?(): void;
|
||||
closeModal?(): void;
|
||||
onOK?(): void;
|
||||
onEscKeypress?(): void;
|
||||
className?: string;
|
||||
modalClassName?: string;
|
||||
bAllowFullSize?: boolean;
|
||||
bDestructiveWarning?: boolean;
|
||||
bDisableBackgroundDismiss?: boolean;
|
||||
bHideCloseIcon?: boolean;
|
||||
bOKDisabled?: boolean;
|
||||
bCancelDisabled?: boolean;
|
||||
}
|
||||
|
||||
export interface ConfirmModalProps extends ModalRootProps {
|
||||
onMiddleButton?(): void; // setting this prop will enable the middle button
|
||||
strTitle?: ReactNode;
|
||||
strDescription?: ReactNode;
|
||||
strOKButtonText?: ReactNode;
|
||||
strCancelButtonText?: ReactNode;
|
||||
strMiddleButtonText?: ReactNode;
|
||||
bAlertDialog?: boolean; // This will open a modal with only OK button enabled
|
||||
bMiddleDisabled?: boolean;
|
||||
}
|
||||
|
||||
export const ConfirmModal = findModuleExport(
|
||||
(e: Export) => !e?.prototype?.OK && e?.prototype?.Cancel && e?.prototype?.render,
|
||||
) as FC<ConfirmModalProps>;
|
||||
|
||||
export const ModalRoot = Object.values(
|
||||
findModule((m: any) => {
|
||||
if (typeof m !== 'object') return false;
|
||||
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.m_mapModalManager && Object.values(m)?.find((x: any) => x?.type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}) || {},
|
||||
)?.find((x: any) => x?.type?.toString()?.includes('((function(){')) as FC<ModalRootProps>;
|
||||
|
||||
interface SimpleModalProps {
|
||||
active?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const ModalModule = findModuleByExport((e: Export) => e?.toString().includes('.ModalPosition,fallback:'), 5);
|
||||
|
||||
const ModalModuleProps = ModalModule ? Object.values(ModalModule) : [];
|
||||
|
||||
export const SimpleModal = ModalModuleProps.find((prop) => {
|
||||
const string = prop?.toString();
|
||||
return string?.includes('.ShowPortalModal()') && string?.includes('.OnElementReadyCallbacks.Register(');
|
||||
}) as FC<SimpleModalProps>;
|
||||
|
||||
export const ModalPosition = ModalModuleProps.find((prop) =>
|
||||
prop?.toString().includes('.ModalPosition,fallback:'),
|
||||
) as FC<SimpleModalProps>;
|
||||
24
src/components/Panel.tsx
Normal file
24
src/components/Panel.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { FC, ReactNode } from 'react';
|
||||
|
||||
import { Export, findModuleDetailsByExport } from '../webpack';
|
||||
|
||||
// TODO where did this go?
|
||||
// export const Panel: FC<{ children?: ReactNode; }> = findModuleExport((e: Export) => {
|
||||
// if (typeof mod !== 'object' || !mod.__esModule) return undefined;
|
||||
// return mod.Panel;
|
||||
// });
|
||||
|
||||
export interface PanelSectionProps {
|
||||
title?: string;
|
||||
spinner?: boolean;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const [mod, panelSection] = findModuleDetailsByExport((e: Export) => e.toString()?.includes('.PanelSection'));
|
||||
|
||||
export const PanelSection = panelSection as FC<PanelSectionProps>;
|
||||
|
||||
export interface PanelSectionRowProps {
|
||||
children?: ReactNode;
|
||||
}
|
||||
export const PanelSectionRow = Object.values(mod).filter((exp: any) => !exp?.toString()?.includes('.PanelSection'))[0] as FC<PanelSectionRowProps>;
|
||||
29
src/components/ProgressBar.tsx
Normal file
29
src/components/ProgressBar.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { ReactNode, VFC } from 'react';
|
||||
|
||||
import { Export, findModuleExport } from '../webpack';
|
||||
import { ItemProps } from './Item';
|
||||
|
||||
export interface ProgressBarItemProps extends ItemProps {
|
||||
indeterminate?: boolean;
|
||||
nTransitionSec?: number;
|
||||
nProgress?: number;
|
||||
focusable?: boolean;
|
||||
}
|
||||
|
||||
export interface ProgressBarProps {
|
||||
indeterminate?: boolean;
|
||||
nTransitionSec?: number;
|
||||
nProgress?: number;
|
||||
focusable?: boolean;
|
||||
}
|
||||
|
||||
export interface ProgressBarWithInfoProps extends ProgressBarItemProps {
|
||||
sTimeRemaining?: ReactNode;
|
||||
sOperationText?: ReactNode;
|
||||
}
|
||||
|
||||
export const ProgressBar = findModuleExport((e: Export) => e?.toString()?.includes('.ProgressBar,"standard"==')) as VFC<ProgressBarProps>;
|
||||
|
||||
export const ProgressBarWithInfo = findModuleExport((e: Export) => e?.toString()?.includes('.ProgressBarFieldStatus},')) as VFC<ProgressBarWithInfoProps>;
|
||||
|
||||
export const ProgressBarItem = findModuleExport((e: Export) => e?.toString()?.includes('"indeterminate","nTransitionSec"')) as VFC<ProgressBarItemProps>;
|
||||
11
src/components/Scroll.tsx
Normal file
11
src/components/Scroll.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { FC, ReactNode } from 'react';
|
||||
|
||||
import { Export, findModuleByExport, findModuleExport } from '../webpack';
|
||||
|
||||
const ScrollingModule = findModuleByExport((e: Export) => e?.render?.toString?.().includes("{case\"x\":"));
|
||||
|
||||
const ScrollingModuleProps = ScrollingModule ? Object.values(ScrollingModule) : [];
|
||||
|
||||
export const ScrollPanel = ScrollingModuleProps.find((prop: any) => prop?.render?.toString?.().includes("{case\"x\":")) as FC<{ children?: ReactNode }>;
|
||||
|
||||
export const ScrollPanelGroup: FC<{ children?: ReactNode }> = findModuleExport((e: Export) => e?.render?.toString().includes(".FocusVisibleChild()),[])"));
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ReactNode, VFC } from 'react';
|
||||
|
||||
import { Module, findModuleChild } from '../webpack';
|
||||
import { Export, findModuleExport } from '../webpack';
|
||||
|
||||
export interface SidebarNavigationPage {
|
||||
title: ReactNode;
|
||||
@@ -23,11 +23,4 @@ export interface SidebarNavigationProps {
|
||||
onPageRequested?: (page: string) => void;
|
||||
}
|
||||
|
||||
export const SidebarNavigation = findModuleChild((mod: Module) => {
|
||||
for (let prop in mod) {
|
||||
if (mod[prop]?.toString()?.includes('"disableRouteReporting"')) {
|
||||
return mod[prop];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}) as VFC<SidebarNavigationProps>;
|
||||
export const SidebarNavigation = findModuleExport((e: Export) => e?.toString()?.includes('"disableRouteReporting"')) as VFC<SidebarNavigationProps>;
|
||||
5
src/components/SteamSpinner.tsx
Executable file
5
src/components/SteamSpinner.tsx
Executable file
@@ -0,0 +1,5 @@
|
||||
import { FC, SVGAttributes } from 'react';
|
||||
|
||||
import { Export, findModuleExport } from '../webpack';
|
||||
|
||||
export const SteamSpinner = findModuleExport((e: Export) => e?.toString?.()?.includes('Steam Spinner') && e?.toString?.()?.includes('src')) as FC<SVGAttributes<SVGElement>>;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FC, ReactNode, createElement, useEffect, useState } from 'react';
|
||||
|
||||
import { fakeRenderComponent, findInReactTree, sleep } from '../utils';
|
||||
import { findModule } from '../webpack';
|
||||
import { Export, findModuleByExport } from '../webpack';
|
||||
import { FooterLegendProps } from './FooterLegend';
|
||||
import { SteamSpinner } from './SteamSpinner';
|
||||
|
||||
@@ -98,13 +98,7 @@ const getTabs = async () => {
|
||||
let oldTabs: any;
|
||||
|
||||
try {
|
||||
const oldTabsModule = findModule((m: any) => {
|
||||
if (typeof m !== 'object') return false;
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.Unbleed) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const oldTabsModule = findModuleByExport((e: Export) => e.Unbleed);
|
||||
if (oldTabsModule)
|
||||
oldTabs = Object.values(oldTabsModule).find((x: any) => x?.type?.toString()?.includes('((function(){'));
|
||||
} catch (e) {
|
||||
25
src/components/index.ts
Executable file
25
src/components/index.ts
Executable file
@@ -0,0 +1,25 @@
|
||||
export * from './Button';
|
||||
export * from './ButtonItem';
|
||||
export * from './Carousel';
|
||||
export * from './ControlsList';
|
||||
export * from './Dialog';
|
||||
export * from './DialogCheckbox';
|
||||
export * from './Dropdown';
|
||||
export * from './Field';
|
||||
export * from './Focusable';
|
||||
export * from './FocusRing';
|
||||
export * from './FooterLegend';
|
||||
export * from './Marquee';
|
||||
export * from './Menu';
|
||||
export * from './Modal';
|
||||
export * from './Panel';
|
||||
export * from './ProgressBar';
|
||||
export * from './SidebarNavigation';
|
||||
export * from './SliderField';
|
||||
export * from './Spinner';
|
||||
export * from './SteamSpinner';
|
||||
export * from './Tabs';
|
||||
export * from './TextField';
|
||||
export * from './Toggle';
|
||||
export * from './ToggleField';
|
||||
export * from './Scroll';
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CSSProperties, FC, useState } from 'react';
|
||||
|
||||
import { ConfirmModal, SliderField, gamepadSliderClasses } from '../deck-components';
|
||||
import { ConfirmModal, SliderField } from '../components';
|
||||
import { gamepadSliderClasses } from '../utils/static-classes';
|
||||
|
||||
interface ColorPickerModalProps {
|
||||
closeModal: () => void;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Fragment, JSXElementConstructor, ReactElement, ReactNode, useEffect, useState } from 'react';
|
||||
|
||||
import { Field, FieldProps, Focusable, GamepadButton } from '../deck-components';
|
||||
import { Field, FieldProps, Focusable, GamepadButton } from '../components';
|
||||
|
||||
/**
|
||||
* A ReorderableList entry of type <T>.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
import { FC, ImgHTMLAttributes, useState } from 'react';
|
||||
|
||||
import { Spinner } from '../deck-components';
|
||||
import { Spinner } from '../components';
|
||||
|
||||
interface SuspensefulImageProps extends ImgHTMLAttributes<HTMLImageElement> {
|
||||
suspenseWidth?: string | number;
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { findModuleChild } from '../webpack';
|
||||
import { FC } from 'react';
|
||||
|
||||
export interface ControlsListProps {
|
||||
alignItems?: 'left' | 'right' | 'center';
|
||||
spacing?: 'standard' | 'extra';
|
||||
}
|
||||
|
||||
export const ControlsList: FC<ControlsListProps> = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return;
|
||||
for (const prop in m) {
|
||||
if (m[prop]?.toString && m[prop].toString().includes('().ControlsListChild') && m[prop].toString().includes('().ControlsListOuterPanel')) {
|
||||
return m[prop];
|
||||
}
|
||||
}
|
||||
return;
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
import { ElementType, FC, ReactNode } from 'react';
|
||||
|
||||
import { findModuleChild } from '../webpack';
|
||||
|
||||
export interface FocusRingProps {
|
||||
className?: string;
|
||||
rootClassName?: string;
|
||||
render?: ElementType;
|
||||
children?: ReactNode;
|
||||
NavigationManager?: any;
|
||||
}
|
||||
|
||||
export const FocusRing = findModuleChild((m: any) => {
|
||||
if (typeof m !== 'object') return false;
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.toString()?.includes('.GetShowDebugFocusRing())')) return m[prop];
|
||||
}
|
||||
return false;
|
||||
}) as FC<FocusRingProps>;
|
||||
@@ -1,26 +0,0 @@
|
||||
import { CSSProperties, FC } from 'react';
|
||||
|
||||
import { findModuleChild } from '../webpack';
|
||||
|
||||
export interface MarqueeProps {
|
||||
play?: boolean;
|
||||
direction?: 'left' | 'right';
|
||||
speed?: number;
|
||||
delay?: number;
|
||||
fadeLength?: number;
|
||||
center?: boolean;
|
||||
resetOnPause?: boolean;
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Marquee: FC<MarqueeProps> = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return;
|
||||
for (const prop in m) {
|
||||
if (m[prop]?.toString && m[prop].toString().includes('.Marquee') && m[prop].toString().includes('--fade-length')) {
|
||||
return m[prop];
|
||||
}
|
||||
}
|
||||
return;
|
||||
});
|
||||
@@ -1,84 +0,0 @@
|
||||
import { FC, ReactNode } from 'react';
|
||||
|
||||
import { fakeRenderComponent } from '../utils';
|
||||
import { findModuleChild } from '../webpack';
|
||||
import { FooterLegendProps } from './FooterLegend';
|
||||
|
||||
export const showContextMenu: (children: ReactNode, parent?: EventTarget) => void = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (typeof m[prop] === 'function' && m[prop].toString().includes('stopPropagation))')) {
|
||||
return m[prop];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export interface MenuProps extends FooterLegendProps {
|
||||
label: string;
|
||||
onCancel?(): void;
|
||||
cancelText?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const Menu: FC<MenuProps> = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.prototype?.HideIfSubmenu && m[prop]?.prototype?.HideMenu) {
|
||||
return m[prop];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export interface MenuGroupProps {
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const MenuGroup: FC<MenuGroupProps> = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
|
||||
for (let prop in m) {
|
||||
if (
|
||||
(m[prop]?.toString()?.includes?.('bInGamepadUI:') &&
|
||||
fakeRenderComponent(() => m[prop]({overview: {appid: 7}}))?.type?.prototype?.RenderSubMenu) ||
|
||||
(m[prop]?.prototype?.RenderSubMenu && m[prop]?.prototype?.ShowSubMenu)
|
||||
) {
|
||||
return m[prop];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export interface MenuItemProps extends FooterLegendProps {
|
||||
bInteractableItem?: boolean;
|
||||
onClick?(evt: Event): void;
|
||||
onSelected?(evt: Event): void;
|
||||
onMouseEnter?(evt: MouseEvent): void;
|
||||
onMoveRight?(): void;
|
||||
selected?: boolean;
|
||||
disabled?: boolean;
|
||||
bPlayAudio?: boolean;
|
||||
tone?: 'positive' | 'emphasis' | 'destructive';
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const MenuItem: FC<MenuItemProps> = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
|
||||
for (let prop in m) {
|
||||
if (
|
||||
m[prop]?.render?.toString()?.includes('bPlayAudio:') ||
|
||||
(m[prop]?.prototype?.OnOKButton && m[prop]?.prototype?.OnMouseEnter)
|
||||
) {
|
||||
return m[prop];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
all().map(m => {
|
||||
if (typeof m !== "object") return undefined;
|
||||
for (let prop in m) { if (m[prop]?.prototype?.OK && m[prop]?.prototype?.Cancel && m[prop]?.prototype?.render) return m[prop]}
|
||||
}).find(x => x)
|
||||
*/
|
||||
@@ -1,178 +0,0 @@
|
||||
import { FC, ReactNode } from 'react';
|
||||
|
||||
import { findSP } from '../utils';
|
||||
import { findModule, findModuleChild } from '../webpack';
|
||||
|
||||
// All of the popout options + strTitle are related. Proper usage is not yet known...
|
||||
export interface ShowModalProps {
|
||||
browserContext?: unknown; // This is another Deck Object that is yet to be found
|
||||
bForcePopOut?: boolean;
|
||||
bHideActionIcons?: boolean;
|
||||
bHideMainWindowForPopouts?: boolean;
|
||||
bNeverPopOut?: boolean;
|
||||
fnOnClose?: () => void; // Seems to be the same as "closeModal" callback, but only when the modal is a popout. Will no longer work after "Update" invocation!
|
||||
popupHeight?: number;
|
||||
popupWidth?: number;
|
||||
promiseRenderComplete?: Promise<void>; // Invoked once the render is complete. Currently, it seems to be used as image loading success/error callback...
|
||||
strTitle?: string;
|
||||
}
|
||||
|
||||
export interface ShowModalResult {
|
||||
// This method will not invoke any of the variations of "closeModal" callbacks!
|
||||
Close: () => void;
|
||||
|
||||
// This method will replace the modal element completely and will not update the callback chains,
|
||||
// meaning that "closeModal" and etc. will not automatically close the modal anymore (also "fnOnClose"
|
||||
// will not be even called upon close anymore)! You have to manually call the "Close" method when, for example,
|
||||
// the "closeModal" is invoked in the newly updated modal:
|
||||
// <ModalRoot closeModal={() => { console.log("ABOUT TO CLOSE"); showModalRes.Close(); }} />
|
||||
Update: (modal: ReactNode) => void;
|
||||
}
|
||||
|
||||
const showModalRaw:
|
||||
| ((
|
||||
modal: ReactNode,
|
||||
parent?: EventTarget,
|
||||
title?: string,
|
||||
props?: ShowModalProps,
|
||||
unknown1?: unknown,
|
||||
hideActions?: { bHideActions?: boolean },
|
||||
modalManager?: unknown,
|
||||
) => ShowModalResult)
|
||||
| void = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (
|
||||
typeof m[prop] === 'function' &&
|
||||
m[prop].toString().includes('props.bDisableBackgroundDismiss') &&
|
||||
!m[prop]?.prototype?.Cancel
|
||||
) {
|
||||
return m[prop];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const oldShowModalRaw: ((modal: ReactNode, parent?: EventTarget, props?: ShowModalProps) => ShowModalResult) | void =
|
||||
findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (typeof m[prop] === 'function' && m[prop].toString().includes('bHideMainWindowForPopouts:!0')) {
|
||||
return m[prop];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const showModal = (
|
||||
modal: ReactNode,
|
||||
parent?: EventTarget,
|
||||
props: ShowModalProps = {
|
||||
strTitle: 'Decky Dialog',
|
||||
bHideMainWindowForPopouts: false,
|
||||
},
|
||||
): ShowModalResult => {
|
||||
if (showModalRaw) {
|
||||
return showModalRaw(modal, parent || findSP(), props.strTitle, props, undefined, {
|
||||
bHideActions: props.bHideActionIcons,
|
||||
});
|
||||
} else if (oldShowModalRaw) {
|
||||
return oldShowModalRaw(modal, parent || findSP(), props);
|
||||
} else {
|
||||
throw new Error('[DFL:Modals]: Cannot find showModal function');
|
||||
}
|
||||
};
|
||||
|
||||
export interface ModalRootProps {
|
||||
children?: ReactNode;
|
||||
onCancel?(): void;
|
||||
closeModal?(): void;
|
||||
onOK?(): void;
|
||||
onEscKeypress?(): void;
|
||||
className?: string;
|
||||
modalClassName?: string;
|
||||
bAllowFullSize?: boolean;
|
||||
bDestructiveWarning?: boolean;
|
||||
bDisableBackgroundDismiss?: boolean;
|
||||
bHideCloseIcon?: boolean;
|
||||
bOKDisabled?: boolean;
|
||||
bCancelDisabled?: boolean;
|
||||
}
|
||||
|
||||
export interface ConfirmModalProps extends ModalRootProps {
|
||||
onMiddleButton?(): void; // setting this prop will enable the middle button
|
||||
strTitle?: ReactNode;
|
||||
strDescription?: ReactNode;
|
||||
strOKButtonText?: ReactNode;
|
||||
strCancelButtonText?: ReactNode;
|
||||
strMiddleButtonText?: ReactNode;
|
||||
bAlertDialog?: boolean; // This will open a modal with only OK button enabled
|
||||
bMiddleDisabled?: boolean;
|
||||
}
|
||||
|
||||
export const ConfirmModal = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (!m[prop]?.prototype?.OK && m[prop]?.prototype?.Cancel && m[prop]?.prototype?.render) {
|
||||
return m[prop];
|
||||
}
|
||||
}
|
||||
}) as FC<ConfirmModalProps>;
|
||||
|
||||
// new as of december 2022 on beta
|
||||
export const ModalRoot = (Object.values(
|
||||
findModule((m: any) => {
|
||||
if (typeof m !== 'object') return false;
|
||||
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.m_mapModalManager && Object.values(m)?.find((x: any) => x?.type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}) || {},
|
||||
)?.find((x: any) => x?.type?.toString()?.includes('((function(){')) ||
|
||||
// before december 2022 beta
|
||||
Object.values(
|
||||
findModule((m: any) => {
|
||||
if (typeof m !== 'object') return false;
|
||||
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.toString()?.includes('"ModalManager","DialogWrapper"')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}) || {},
|
||||
)?.find((x: any) => x?.type?.toString()?.includes('((function(){')) ||
|
||||
// old
|
||||
findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.prototype?.OK && m[prop]?.prototype?.Cancel && m[prop]?.prototype?.render) {
|
||||
return m[prop];
|
||||
}
|
||||
}
|
||||
})) as FC<ModalRootProps>;
|
||||
|
||||
interface SimpleModalProps {
|
||||
active?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const ModalModule = findModule((mod: any) => {
|
||||
if (typeof mod !== 'object') return false;
|
||||
for (let prop in mod) {
|
||||
if (Object.keys(mod).length > 4 && mod[prop]?.toString().includes('.ModalPosition,fallback:')) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const ModalModuleProps = ModalModule ? Object.values(ModalModule) : [];
|
||||
|
||||
export const SimpleModal = ModalModuleProps.find(prop => {
|
||||
const string = prop?.toString()
|
||||
return string?.includes(".ShowPortalModal()") && string?.includes(".OnElementReadyCallbacks.Register(")
|
||||
}) as FC<SimpleModalProps>;
|
||||
|
||||
export const ModalPosition = ModalModuleProps.find(prop => prop?.toString().includes(".ModalPosition,fallback:")) as FC<SimpleModalProps>;
|
||||
@@ -1,33 +0,0 @@
|
||||
import { FC, ReactNode } from 'react';
|
||||
|
||||
import { findModuleChild } from '../webpack';
|
||||
|
||||
export const Panel: FC<{ children?: ReactNode; }> = findModuleChild((mod) => {
|
||||
if (typeof mod !== 'object' || !mod.__esModule) return undefined;
|
||||
return mod.Panel;
|
||||
})
|
||||
|
||||
export interface PanelSectionProps {
|
||||
title?: string;
|
||||
spinner?: boolean;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const [panelSection, mod] = findModuleChild((mod: any) => {
|
||||
for (let prop in mod) {
|
||||
if (mod[prop]?.toString()?.includes('.PanelSection')) {
|
||||
return [mod[prop], mod];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
export const PanelSection = panelSection as FC<PanelSectionProps>;
|
||||
|
||||
export interface PanelSectionRowProps {
|
||||
children?: ReactNode;
|
||||
}
|
||||
// New as of Feb 22 2023 Beta || Old
|
||||
export const PanelSectionRow =
|
||||
(mod.PanelSectionRow ||
|
||||
Object.values(mod).filter((exp: any) => !exp?.toString()?.includes('.PanelSection'))[0]) as FC<PanelSectionRowProps>;
|
||||
@@ -1,44 +0,0 @@
|
||||
import { ReactNode, VFC } from 'react';
|
||||
|
||||
import { findModuleChild } from '../webpack';
|
||||
import { ItemProps } from './Item';
|
||||
|
||||
export interface ProgressBarItemProps extends ItemProps {
|
||||
indeterminate?: boolean;
|
||||
nTransitionSec?: number;
|
||||
nProgress?: number;
|
||||
focusable?: boolean;
|
||||
}
|
||||
|
||||
export interface ProgressBarProps {
|
||||
indeterminate?: boolean;
|
||||
nTransitionSec?: number;
|
||||
nProgress?: number;
|
||||
focusable?: boolean;
|
||||
}
|
||||
|
||||
export interface ProgressBarWithInfoProps extends ProgressBarItemProps {
|
||||
sTimeRemaining?: ReactNode;
|
||||
sOperationText?: ReactNode;
|
||||
}
|
||||
|
||||
export const ProgressBar = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.toString()?.includes('.ProgressBar,"standard"==')) return m[prop];
|
||||
}
|
||||
}) as VFC<ProgressBarProps>;
|
||||
|
||||
export const ProgressBarWithInfo = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.toString()?.includes('.ProgressBarFieldStatus},')) return m[prop];
|
||||
}
|
||||
}) as VFC<ProgressBarWithInfoProps>;
|
||||
|
||||
export const ProgressBarItem = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.toString()?.includes('"indeterminate","nTransitionSec"')) return m[prop];
|
||||
}
|
||||
}) as VFC<ProgressBarItemProps>;
|
||||
@@ -1,22 +0,0 @@
|
||||
import { FC, ReactNode } from 'react';
|
||||
|
||||
import { findModule, findModuleChild } from '../webpack';
|
||||
|
||||
const ScrollingModule = findModule((mod) => {
|
||||
if (typeof mod !== 'object') return false;
|
||||
for (let prop in mod) {
|
||||
if (mod[prop]?.render?.toString?.().includes("{case\"x\":")) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const ScrollingModuleProps = ScrollingModule ? Object.values(ScrollingModule) : [];
|
||||
|
||||
export const ScrollPanel = ScrollingModuleProps.find((prop: any) => prop?.render?.toString?.().includes("{case\"x\":")) as FC<{ children?: ReactNode }>;
|
||||
|
||||
export const ScrollPanelGroup: FC<{ children?: ReactNode }> = findModuleChild((mod) => {
|
||||
if (typeof mod !== 'object') return undefined;
|
||||
for (let prop in mod) {
|
||||
if (mod[prop]?.render?.toString().includes(".FocusVisibleChild()),[])")) return mod[prop];
|
||||
}
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
import { FC, SVGAttributes } from 'react';
|
||||
|
||||
import { findModuleChild } from '../webpack';
|
||||
|
||||
export const SteamSpinner = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (
|
||||
m[prop]?.toString?.()?.includes('Steam Spinner') && m[prop]?.toString?.()?.includes('src')
|
||||
)
|
||||
return m[prop];
|
||||
}
|
||||
}) as FC<SVGAttributes<SVGElement>>;
|
||||
@@ -1,3 +1,7 @@
|
||||
declare global {
|
||||
var SteamClient: SteamClient;
|
||||
}
|
||||
|
||||
export interface Apps {
|
||||
RegisterForAppOverviewChanges: any;
|
||||
RegisterForAppDetails: any;
|
||||
2
src/globals/index.ts
Normal file
2
src/globals/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./SteamClient";
|
||||
export * from "./stores";
|
||||
34
src/deck-components/index.ts → src/globals/stores.ts
Executable file → Normal file
34
src/deck-components/index.ts → src/globals/stores.ts
Executable file → Normal file
@@ -1,37 +1,5 @@
|
||||
export * from './Button';
|
||||
export * from './ButtonItem';
|
||||
export * from './Carousel';
|
||||
export * from './ControlsList';
|
||||
export * from './Dialog';
|
||||
export * from './DialogCheckbox';
|
||||
export * from './Dropdown';
|
||||
export * from './Field';
|
||||
export * from './Focusable';
|
||||
export * from './FocusRing';
|
||||
export * from './FooterLegend';
|
||||
export * from './Marquee';
|
||||
export * from './Menu';
|
||||
export * from './Modal';
|
||||
export * from './Panel';
|
||||
export * from './ProgressBar';
|
||||
export * from './Router';
|
||||
export * from './SidebarNavigation';
|
||||
export * from './SliderField';
|
||||
export * from './Spinner';
|
||||
export * from './static-classes';
|
||||
export * from './SteamSpinner';
|
||||
export * from './Tabs';
|
||||
export * from './TextField';
|
||||
export * from './Toggle';
|
||||
export * from './ToggleField';
|
||||
export * from './SteamClient';
|
||||
export * from './Scroll';
|
||||
|
||||
import { AppDetails, LogoPosition, SteamAppOverview, SteamClient } from './SteamClient';
|
||||
|
||||
import { AppDetails, LogoPosition, SteamAppOverview } from './SteamClient';
|
||||
declare global {
|
||||
var SteamClient: SteamClient;
|
||||
|
||||
interface Window {
|
||||
LocalizationManager: {
|
||||
m_mapTokens: Map<string, string>;
|
||||
@@ -1,8 +1,10 @@
|
||||
// export * from './deck-libs';
|
||||
export * from './custom-components';
|
||||
export * from './custom-hooks';
|
||||
export * from './deck-components';
|
||||
export * from './components';
|
||||
export * from './deck-hooks';
|
||||
export * from './modules';
|
||||
export * from './globals';
|
||||
export * from './plugin';
|
||||
export * from './webpack';
|
||||
export * from './utils';
|
||||
|
||||
94
src/logger.ts
Normal file
94
src/logger.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
const bgStyle1 = 'background: #16a085; color: black;';
|
||||
|
||||
export const log = (name: string, ...args: any[]) => {
|
||||
console.log(
|
||||
`%c @decky/ui %c ${name} %c`,
|
||||
bgStyle1,
|
||||
'background: #1abc9c; color: black;',
|
||||
'background: transparent;',
|
||||
...args,
|
||||
);
|
||||
};
|
||||
|
||||
export const group = (name: string, ...args: any[]) => {
|
||||
console.group(
|
||||
`%c @decky/ui %c ${name} %c`,
|
||||
bgStyle1,
|
||||
'background: #1abc9c; color: black;',
|
||||
'background: transparent;',
|
||||
...args,
|
||||
);
|
||||
};
|
||||
|
||||
export const groupEnd = (name: string, ...args: any[]) => {
|
||||
console.groupEnd();
|
||||
if (args?.length > 0) console.log(
|
||||
`^ %c @decky/ui %c ${name} %c`,
|
||||
bgStyle1,
|
||||
'background: #1abc9c; color: black;',
|
||||
'background: transparent;',
|
||||
...args,
|
||||
);
|
||||
};
|
||||
|
||||
export const debug = (name: string, ...args: any[]) => {
|
||||
console.debug(
|
||||
`%c @decky/ui %c ${name} %c`,
|
||||
bgStyle1,
|
||||
'background: #1abc9c; color: black;',
|
||||
'color: blue;',
|
||||
...args,
|
||||
);
|
||||
};
|
||||
|
||||
export const warn = (name: string, ...args: any[]) => {
|
||||
console.warn(
|
||||
`%c @decky/ui %c ${name} %c`,
|
||||
bgStyle1,
|
||||
'background: #ffbb00; color: black;',
|
||||
'color: blue;',
|
||||
...args,
|
||||
);
|
||||
};
|
||||
|
||||
export const error = (name: string, ...args: any[]) => {
|
||||
console.error(
|
||||
`%c @decky/ui %c ${name} %c`,
|
||||
bgStyle1,
|
||||
'background: #FF0000;',
|
||||
'background: transparent;',
|
||||
...args,
|
||||
);
|
||||
};
|
||||
|
||||
class Logger {
|
||||
constructor(private name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
log(...args: any[]) {
|
||||
log(this.name, ...args);
|
||||
}
|
||||
|
||||
debug(...args: any[]) {
|
||||
debug(this.name, ...args);
|
||||
}
|
||||
|
||||
warn(...args: any[]) {
|
||||
warn(this.name, ...args);
|
||||
}
|
||||
|
||||
error(...args: any[]) {
|
||||
error(this.name, ...args);
|
||||
}
|
||||
|
||||
group(...args: any[]) {
|
||||
group(this.name, ...args);
|
||||
}
|
||||
|
||||
groupEnd(...args: any[]) {
|
||||
groupEnd(this.name, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
export default Logger;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { sleep } from '../utils';
|
||||
import { Module, findModuleChild } from '../webpack';
|
||||
import { Export, findModuleExport } from '../webpack';
|
||||
|
||||
export enum SideMenu {
|
||||
None,
|
||||
@@ -101,12 +101,7 @@ export interface Router {
|
||||
get MainRunningApp(): AppOverview | undefined;
|
||||
}
|
||||
|
||||
export const Router = findModuleChild((m: Module) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.Navigate && m[prop]?.NavigationManager) return m[prop];
|
||||
}
|
||||
}) as Router;
|
||||
export const Router = findModuleExport((e: Export) => e.Navigate && e.NavigationManager) as Router;
|
||||
|
||||
export interface Navigation {
|
||||
Navigate(path: string): void;
|
||||
@@ -133,14 +128,7 @@ try {
|
||||
if (!Router.NavigateToAppProperties || (Router as unknown as any).deckyShim) {
|
||||
function initInternalNavigators() {
|
||||
try {
|
||||
InternalNavigators = findModuleChild((m: any) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.GetNavigator && m[prop]?.SetNavigator) {
|
||||
return m[prop];
|
||||
}
|
||||
}
|
||||
})?.GetNavigator();
|
||||
InternalNavigators = findModuleExport((e: Export) => e.GetNavigator && e.SetNavigator)?.GetNavigator();
|
||||
} catch (e) {
|
||||
console.error('[DFL:Router]: Failed to init internal navigators, trying again');
|
||||
}
|
||||
1
src/modules/index.ts
Normal file
1
src/modules/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './Router';
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './patcher';
|
||||
export * from './react';
|
||||
export * from './static-classes';
|
||||
|
||||
declare global {
|
||||
var FocusNavController: any;
|
||||
|
||||
@@ -67,15 +67,15 @@ export function wrapReactClass(node: any, prop: any = 'type') {
|
||||
|
||||
export function getReactRoot(o: HTMLElement | Element | Node) {
|
||||
return (
|
||||
o[Object.keys(o).find((k) => k.startsWith('__reactContainer$')) as string] ||
|
||||
o['_reactRootContainer']?._internalRoot?.current
|
||||
// @ts-expect-error 7053
|
||||
o[Object.keys(o).find((k) => k.startsWith('__reactContainer$')) as string] || o['_reactRootContainer']?._internalRoot?.current
|
||||
);
|
||||
}
|
||||
|
||||
export function getReactInstance(o: HTMLElement | Element | Node) {
|
||||
return (
|
||||
o[Object.keys(o).find((k) => k.startsWith('__reactFiber')) as string] ||
|
||||
o[Object.keys(o).find((k) => k.startsWith('__reactInternalInstance')) as string]
|
||||
// @ts-expect-error 7053
|
||||
o[Object.keys(o).find((k) => k.startsWith('__reactFiber')) as string] || o[Object.keys(o).find((k) => k.startsWith('__reactInternalInstance')) as string]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
114
src/webpack.ts
114
src/webpack.ts
@@ -1,65 +1,99 @@
|
||||
import Logger from "./logger";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
webpackJsonp: any;
|
||||
webpackChunksteamui: any;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
const logger = new Logger("Webpack");
|
||||
|
||||
// In most case an object with getters for each property. Look for the first call to r.d in the module, usually near or at the top.
|
||||
export type Module = any;
|
||||
export type Export = any;
|
||||
type FilterFn = (module: any) => boolean;
|
||||
type ExportFilterFn = (moduleExport: any, exportName?: any) => boolean;
|
||||
type FindFn = (module: any) => any;
|
||||
|
||||
export let webpackCache: any = {};
|
||||
let hasWebpack5 = false;
|
||||
export let modules: any = [];
|
||||
|
||||
if (window.webpackJsonp && !window.webpackJsonp.deckyShimmed) {
|
||||
// Webpack 4, currently on stable
|
||||
const wpRequire = window.webpackJsonp.push([
|
||||
[],
|
||||
{ get_require: (mod: any, _exports: any, wpRequire: any) => (mod.exports = wpRequire) },
|
||||
[['get_require']],
|
||||
]);
|
||||
|
||||
delete wpRequire.m.get_require;
|
||||
delete wpRequire.c.get_require;
|
||||
webpackCache = wpRequire.c;
|
||||
} else {
|
||||
function initModuleCache() {
|
||||
const startTime = performance.now();
|
||||
logger.group("Webpack Module Init");
|
||||
// Webpack 5, currently on beta
|
||||
hasWebpack5 = true;
|
||||
const id = Math.random();
|
||||
let initReq: any;
|
||||
// Generate a fake module ID
|
||||
const id = Math.random(); // really should be an int and not a float but who cares
|
||||
let webpackRequire!: ((id: any) => Module) & {m: object};
|
||||
// Insert our module in a new chunk.
|
||||
// The module will then be called with webpack's internal require function as its first argument
|
||||
window.webpackChunksteamui.push([
|
||||
[id],
|
||||
{},
|
||||
(r: any) => {
|
||||
initReq = r;
|
||||
webpackRequire = r;
|
||||
},
|
||||
]);
|
||||
for (let i of Object.keys(initReq.m)) {
|
||||
|
||||
logger.log("Initializing all modules. Errors here likely do not matter, as they are usually just failing module side effects.");
|
||||
|
||||
// Loop over every module ID
|
||||
for (let i of Object.keys(webpackRequire.m)) {
|
||||
try {
|
||||
webpackCache[i] = initReq(i);
|
||||
const module = webpackRequire(i);
|
||||
if (module) {
|
||||
modules.push(module);
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug("[DFL:Webpack]: Ignoring require error for module", i, e);
|
||||
logger.debug("Ignoring require error for module", i, e);
|
||||
}
|
||||
}
|
||||
|
||||
logger.groupEnd(`Modules initialized in ${performance.now() - startTime}ms...`);
|
||||
}
|
||||
|
||||
export const allModules: Module[] = hasWebpack5
|
||||
? Object.values(webpackCache).filter((x) => x)
|
||||
: Object.keys(webpackCache)
|
||||
.map((x) => webpackCache[x].exports)
|
||||
.filter((x) => x);
|
||||
initModuleCache();
|
||||
|
||||
export const findModule = (filter: FilterFn) => {
|
||||
for (const m of allModules) {
|
||||
for (const m of modules) {
|
||||
if (m.default && filter(m.default)) return m.default;
|
||||
if (filter(m)) return m;
|
||||
}
|
||||
};
|
||||
|
||||
export const findModuleDetailsByExport = (filter: ExportFilterFn, minExports?: number): [module: Module | undefined, moduleExport: any, exportName: any] => {
|
||||
for (const m of modules) {
|
||||
if (!m) continue;
|
||||
for (const mod of [m.default, m]) {
|
||||
if (typeof mod !== 'object') continue;
|
||||
if (minExports && Object.keys(mod).length < minExports) continue;
|
||||
for (let exportName in mod) {
|
||||
if (mod?.[exportName]) {
|
||||
const filterRes = filter(mod[exportName], exportName);
|
||||
if (filterRes) {
|
||||
return [mod, mod[exportName], exportName];
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return [undefined, undefined, undefined];
|
||||
}
|
||||
|
||||
export const findModuleByExport = (filter: ExportFilterFn, minExports?: number) => {
|
||||
return findModuleDetailsByExport(filter, minExports)?.[0];
|
||||
}
|
||||
|
||||
export const findModuleExport = (filter: ExportFilterFn, minExports?: number) => {
|
||||
return findModuleDetailsByExport(filter, minExports)?.[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use findModuleExport instead
|
||||
*/
|
||||
export const findModuleChild = (filter: FindFn) => {
|
||||
for (const m of allModules) {
|
||||
for (const m of modules) {
|
||||
for (const mod of [m.default, m]) {
|
||||
const filterRes = filter(mod);
|
||||
if (filterRes) {
|
||||
@@ -74,7 +108,7 @@ export const findModuleChild = (filter: FindFn) => {
|
||||
export const findAllModules = (filter: FilterFn) => {
|
||||
const out = [];
|
||||
|
||||
for (const m of allModules) {
|
||||
for (const m of modules) {
|
||||
if (m.default && filter(m.default)) out.push(m.default);
|
||||
if (filter(m)) out.push(m);
|
||||
}
|
||||
@@ -82,7 +116,7 @@ export const findAllModules = (filter: FilterFn) => {
|
||||
return out;
|
||||
};
|
||||
|
||||
export const CommonUIModule = allModules.find((m: Module) => {
|
||||
export const CommonUIModule = modules.find((m: Module) => {
|
||||
if (typeof m !== 'object') return false;
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.contextType?._currentValue && Object.keys(m).length > 60) return true;
|
||||
@@ -90,18 +124,6 @@ export const CommonUIModule = allModules.find((m: Module) => {
|
||||
return false;
|
||||
});
|
||||
|
||||
export const IconsModule = findModule((m: Module) => {
|
||||
if (typeof m !== 'object') return false;
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.toString && /Spinner\)}\),.\.createElement\(\"path\",{d:\"M18 /.test(m[prop].toString())) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
export const IconsModule = findModuleByExport(e => e?.toString && /Spinner\)}\),.\.createElement\(\"path\",{d:\"M18 /.test(e.toString()));
|
||||
|
||||
export const ReactRouter = allModules.find((m: Module) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.computeRootMatch) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
export const ReactRouter = findModuleByExport(e => e.computeRootMatch);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"noImplicitThis": true,
|
||||
"noImplicitAny": true,
|
||||
"strict": true,
|
||||
"suppressImplicitAnyIndexErrors": true,
|
||||
"removeComments": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user