Skip to content

Commit

Permalink
feat(*): add v4 webpack api and port everything to it
Browse files Browse the repository at this point in the history
also restructures a bunch
  • Loading branch information
AAGaming00 committed May 12, 2024
1 parent 9c79187 commit bffd530
Show file tree
Hide file tree
Showing 52 changed files with 508 additions and 581 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 changes: 9 additions & 0 deletions src/components/ControlsList.tsx
Original file line number Diff line number Diff line change
@@ -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'));
File renamed without changes.
File renamed without changes.
File renamed without changes.
9 changes: 2 additions & 7 deletions src/deck-components/Field.tsx → src/components/Field.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 changes: 13 additions & 0 deletions src/components/FocusRing.ts
Original file line number Diff line number Diff line change
@@ -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>;
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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>>;
File renamed without changes.
File renamed without changes.
18 changes: 18 additions & 0 deletions src/components/Marquee.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 57 additions & 0 deletions src/components/Menu.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 119 additions & 0 deletions src/components/Modal.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 24 additions & 0 deletions src/components/Panel.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 29 additions & 0 deletions src/components/ProgressBar.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 11 additions & 0 deletions src/components/Scroll.tsx
Original file line number Diff line number Diff line change
@@ -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()),[])"));
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ReactNode, VFC } from 'react';

import { Module, findModuleChild } from '../webpack';
import { Export, findModuleExport } from '../webpack';

export interface SidebarNavigationPage {
title: ReactNode;
Expand All @@ -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>;
File renamed without changes.
File renamed without changes.
5 changes: 5 additions & 0 deletions src/components/SteamSpinner.tsx
Original file line number Diff line number Diff line change
@@ -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>>;
Loading

0 comments on commit bffd530

Please sign in to comment.