diff --git a/package.json b/package.json index dba841e..4bd14d0 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "husky": "^8.0.1", "import-sort-style-module": "^6.0.0", "jest": "^27.5.1", + "prettier": "^2.7.1", "prettier-plugin-import-sort": "^0.0.7", "semantic-release": "^19.0.3", "shx": "^0.3.4", diff --git a/src/custom-components/ColorPickerModal.tsx b/src/custom-components/ColorPickerModal.tsx index 743dc5e..a783d4a 100644 --- a/src/custom-components/ColorPickerModal.tsx +++ b/src/custom-components/ColorPickerModal.tsx @@ -1,5 +1,6 @@ -import { gamepadSliderClasses, ConfirmModal, SliderField } from "../deck-components"; -import { useState, FC, CSSProperties } from "react"; +import { CSSProperties, FC, useState } from 'react'; + +import { ConfirmModal, SliderField, gamepadSliderClasses } from '../deck-components'; interface ColorPickerModalProps { closeModal: () => void; @@ -14,7 +15,7 @@ interface ColorPickerModalProps { export const ColorPickerModal: FC = ({ closeModal, onConfirm = () => {}, - title = "Color Picker", + title = 'Color Picker', defaultH = 0, defaultS = 100, defaultL = 50, @@ -26,22 +27,22 @@ export const ColorPickerModal: FC = ({ const [A, setA] = useState(defaultA); const colorPickerCSSVars = { - "--decky-color-picker-hvalue": `${H}`, - "--decky-color-picker-svalue": `${S}%`, - "--decky-color-picker-lvalue": `${L}%`, - "--decky-color-picker-avalue": `${A}`, + '--decky-color-picker-hvalue': `${H}`, + '--decky-color-picker-svalue': `${S}%`, + '--decky-color-picker-lvalue': `${L}%`, + '--decky-color-picker-avalue': `${A}`, } as CSSProperties; return ( - { - onConfirm(`hsla(${H}, ${S}%, ${L}%, ${A})`); - closeModal(); - }} - > - +
+
+ + {title} + +
-
- - {title} - -
-
+ >
+
+
+
+
-
-
- -
-
- -
-
- -
-
- -
+
+
- +
+ +
+
+ +
+
+ ); }; diff --git a/src/custom-components/SuspensefulImage.tsx b/src/custom-components/SuspensefulImage.tsx index 0e2d02f..7451ea0 100644 --- a/src/custom-components/SuspensefulImage.tsx +++ b/src/custom-components/SuspensefulImage.tsx @@ -1,7 +1,8 @@ -import { Spinner } from '../deck-components'; import { useEffect } from 'react'; import { FC, ImgHTMLAttributes, useState } from 'react'; +import { Spinner } from '../deck-components'; + interface SuspensefulImageProps extends ImgHTMLAttributes { suspenseWidth?: string | number; suspenseHeight?: string | number; @@ -38,4 +39,4 @@ export const SuspensefulImage: FC = (props) => { ) : ( ); -}; \ No newline at end of file +}; diff --git a/src/custom-hooks/usequickaccessvisible.tsx b/src/custom-hooks/usequickaccessvisible.tsx index e66de38..62e4708 100644 --- a/src/custom-hooks/usequickaccessvisible.tsx +++ b/src/custom-hooks/usequickaccessvisible.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useEffect, useState } from 'react'; declare global { var FocusNavController: any; @@ -7,15 +7,15 @@ declare global { /** * Returns state indicating the visibility of quick access menu. * - * @remarks + * @remarks * During development it is possible to open the quick access menu without giving it * focus in some cases. In such cases, the quick access menu state is invisible. - * + * * This seems to be impossible to replicate when running the deck normally. Even in * the edge cases it always seems to have a focus. - * + * * @returns `true` if quick access menu is visible (focused) and `false` otherwise. - * + * * @example * import { VFC, useEffect } from "react"; * import { useQuickAccessVisible } from "decky-frontend-lib"; @@ -46,20 +46,21 @@ export function useQuickAccessVisible(): boolean { const [isVisible, setIsVisible] = useState(true); useEffect(() => { - const quickAccessWindow: Window | null = FocusNavController?.GetGamepadNavTreeByID("QuickAccess-NA")?.m_Root?.m_element?.ownerDocument.defaultView ?? null; + const quickAccessWindow: Window | null = + FocusNavController?.GetGamepadNavTreeByID('QuickAccess-NA')?.m_Root?.m_element?.ownerDocument.defaultView ?? null; if (quickAccessWindow === null) { - console.error("Could not get window of QuickAccess menu!"); + console.error('Could not get window of QuickAccess menu!'); return; } const onBlur = () => setIsVisible(false); const onFocus = () => setIsVisible(true); - quickAccessWindow.addEventListener("blur", onBlur); - quickAccessWindow.addEventListener("focus", onFocus); + quickAccessWindow.addEventListener('blur', onBlur); + quickAccessWindow.addEventListener('focus', onFocus); return () => { - quickAccessWindow.removeEventListener("blur", onBlur); - quickAccessWindow.removeEventListener("focus", onFocus); + quickAccessWindow.removeEventListener('blur', onBlur); + quickAccessWindow.removeEventListener('focus', onFocus); }; }, []); diff --git a/src/deck-components/Button.tsx b/src/deck-components/Button.tsx index 9fee9c6..ba67a16 100644 --- a/src/deck-components/Button.tsx +++ b/src/deck-components/Button.tsx @@ -1,8 +1,8 @@ import { FC } from 'react'; -import { DialogButton, DialogButtonProps } from "./Dialog"; -export interface ButtonProps extends DialogButtonProps { -} +import { DialogButton, DialogButtonProps } from './Dialog'; + +export interface ButtonProps extends DialogButtonProps {} // Button isn't exported, so call DialogButton to grab it export const Button = (DialogButton as any)?.render({}).type as FC; diff --git a/src/deck-components/ButtonItem.tsx b/src/deck-components/ButtonItem.tsx index 390bafd..e27b309 100644 --- a/src/deck-components/ButtonItem.tsx +++ b/src/deck-components/ButtonItem.tsx @@ -8,6 +8,8 @@ export interface ButtonItemProps extends ItemProps { disabled?: boolean; } -export const ButtonItem = Object.values(CommonUIModule).find((mod: any) => - mod?.render?.toString()?.includes('"highlightOnFocus","childrenContainerWidth"') || mod?.render?.toString()?.includes('childrenContainerWidth:"min"'), +export const ButtonItem = Object.values(CommonUIModule).find( + (mod: any) => + mod?.render?.toString()?.includes('"highlightOnFocus","childrenContainerWidth"') || + mod?.render?.toString()?.includes('childrenContainerWidth:"min"'), ) as FC; diff --git a/src/deck-components/Carousel.ts b/src/deck-components/Carousel.ts index 9eff157..ecfb760 100644 --- a/src/deck-components/Carousel.ts +++ b/src/deck-components/Carousel.ts @@ -1,5 +1,6 @@ -import { HTMLAttributes, ReactNode, RefAttributes, VFC } from "react"; -import { findModuleChild } from "../webpack"; +import { HTMLAttributes, ReactNode, RefAttributes, VFC } from 'react'; + +import { findModuleChild } from '../webpack'; export interface CarouselProps extends HTMLAttributes { autoFocus?: boolean; @@ -22,7 +23,6 @@ export interface CarouselProps extends HTMLAttributes { 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]; + if (m[prop]?.render?.toString().includes('setFocusedColumn:')) return m[prop]; } -}) as VFC>; \ No newline at end of file +}) as VFC>; diff --git a/src/deck-components/Dialog.tsx b/src/deck-components/Dialog.tsx index dd88e5c..111d132 100644 --- a/src/deck-components/Dialog.tsx +++ b/src/deck-components/Dialog.tsx @@ -1,5 +1,6 @@ -import { CommonUIModule } from "../webpack"; -import { CSSProperties, FC, RefAttributes } from "react"; +import { CSSProperties, FC, RefAttributes } from 'react'; + +import { CommonUIModule } from '../webpack'; import { FooterLegendProps } from './FooterLegend'; export interface DialogCommonProps extends RefAttributes { @@ -22,35 +23,37 @@ export interface DialogButtonProps extends DialogCommonProps, FooterLegendProps onSubmit?(e: SubmitEvent): void; } -const CommonDialogDivs = Object.values(CommonUIModule).filter((m: any) => typeof m === "object" && m?.render?.toString().includes('"div",Object.assign({},')); -const MappedDialogDivs = new Map(Object.values(CommonDialogDivs).map((m: any) => { - const renderedDiv = m.render({}); - // Take only the first class name segment as it identifies the element we want - return [renderedDiv.props.className.split(" ")[0], m] -})); +const CommonDialogDivs = Object.values(CommonUIModule).filter( + (m: any) => typeof m === 'object' && m?.render?.toString().includes('"div",Object.assign({},'), +); +const MappedDialogDivs = new Map( + Object.values(CommonDialogDivs).map((m: any) => { + const renderedDiv = m.render({}); + // Take only the first class name segment as it identifies the element we want + return [renderedDiv.props.className.split(' ')[0], m]; + }), +); -export const DialogHeader = MappedDialogDivs.get("DialogHeader") as FC; -export const DialogSubHeader = MappedDialogDivs.get("DialogSubHeader") as FC; -export const DialogFooter = MappedDialogDivs.get("DialogFooter") as FC; -export const DialogLabel = MappedDialogDivs.get("DialogLabel") as FC; -export const DialogBodyText = MappedDialogDivs.get("DialogBodyText") as FC; -export const DialogBody = MappedDialogDivs.get("DialogBody") as FC; -export const DialogControlsSection = MappedDialogDivs.get("DialogControlsSection") as FC; -export const DialogControlsSectionHeader = MappedDialogDivs.get("DialogControlsSectionHeader") as FC; +export const DialogHeader = MappedDialogDivs.get('DialogHeader') as FC; +export const DialogSubHeader = MappedDialogDivs.get('DialogSubHeader') as FC; +export const DialogFooter = MappedDialogDivs.get('DialogFooter') as FC; +export const DialogLabel = MappedDialogDivs.get('DialogLabel') as FC; +export const DialogBodyText = MappedDialogDivs.get('DialogBodyText') as FC; +export const DialogBody = MappedDialogDivs.get('DialogBody') as FC; +export const DialogControlsSection = MappedDialogDivs.get('DialogControlsSection') as FC; +export const DialogControlsSectionHeader = MappedDialogDivs.get('DialogControlsSectionHeader') as FC; export const DialogButtonPrimary = Object.values(CommonUIModule).find( - (mod: any) => - mod?.render?.toString()?.includes('DialogButton') && - mod?.render?.toString()?.includes('Primary') + (mod: any) => mod?.render?.toString()?.includes('DialogButton') && mod?.render?.toString()?.includes('Primary'), ) as FC; export const DialogButtonSecondary = Object.values(CommonUIModule).find( (mod: any) => mod?.render?.toString()?.includes('Object.assign({type:"button"') && mod?.render?.toString()?.includes('DialogButton') && - mod?.render?.toString()?.includes('Secondary') + mod?.render?.toString()?.includes('Secondary'), ) as FC; -// This is the "main" button. The Primary can act as a submit button, +// This is the "main" button. The Primary can act as a submit button, // therefore secondary is chosen (also for backwards comp. reasons) export const DialogButton = DialogButtonSecondary; diff --git a/src/deck-components/Field.tsx b/src/deck-components/Field.tsx index ea4c5d4..9ac37d5 100644 --- a/src/deck-components/Field.tsx +++ b/src/deck-components/Field.tsx @@ -1,4 +1,5 @@ import { FC, HTMLAttributes, ReactNode, RefAttributes } from 'react'; + import { findModuleChild } from '../webpack'; import { FooterLegendProps } from './FooterLegend'; @@ -11,7 +12,7 @@ export interface FieldProps extends HTMLAttributes, FooterLegend inlineWrap?: 'keep-inline' | 'shift-children-below'; // If label is too long it will move shildren below before starting to wrap label childrenLayout?: 'below' | 'inline'; childrenContainerWidth?: 'min' | 'max' | 'fixed'; // Does not work with childrenLayout==='below' - spacingBetweenLabelAndChild?: 'none'; // This applies only when childrenLayout==='below' + spacingBetweenLabelAndChild?: 'none'; // This applies only when childrenLayout==='below' padding?: 'none' | 'standard' | 'compact'; className?: string; highlightOnFocus?: boolean; @@ -20,8 +21,8 @@ export interface FieldProps extends HTMLAttributes, FooterLegend } 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] - } + 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>; diff --git a/src/deck-components/FocusRing.ts b/src/deck-components/FocusRing.ts index d949eb2..93290ab 100644 --- a/src/deck-components/FocusRing.ts +++ b/src/deck-components/FocusRing.ts @@ -1,18 +1,19 @@ -import { ElementType, FC, ReactNode } from "react"; -import { findModuleChild } from "../webpack"; +import { ElementType, FC, ReactNode } from 'react'; + +import { findModuleChild } from '../webpack'; export interface FocusRingProps { - className?: string, - rootClassName?: string, - render?: ElementType, - children?: ReactNode, - NavigationManager?: any + 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; \ No newline at end of file + if (typeof m !== 'object') return false; + for (let prop in m) { + if (m[prop]?.toString()?.includes('.GetShowDebugFocusRing())')) return m[prop]; + } + return false; +}) as FC; diff --git a/src/deck-components/Focusable.tsx b/src/deck-components/Focusable.tsx index 497e6d8..eb49d97 100644 --- a/src/deck-components/Focusable.tsx +++ b/src/deck-components/Focusable.tsx @@ -1,10 +1,11 @@ -import { HTMLAttributes, ReactNode, RefAttributes, VFC } from "react"; -import { findModuleChild } from "../webpack"; -import { FooterLegendProps } from "./FooterLegend"; +import { HTMLAttributes, ReactNode, RefAttributes, VFC } from 'react'; + +import { findModuleChild } from '../webpack'; +import { FooterLegendProps } from './FooterLegend'; export interface FocusableProps extends HTMLAttributes, FooterLegendProps { children: ReactNode; - "flow-children"?: string; + 'flow-children'?: string; focusClassName?: string; focusWithinClassName?: string; onActivate?: (e: CustomEvent) => void; @@ -17,4 +18,4 @@ export const Focusable = findModuleChild((m) => { if (m[prop]?.render?.toString()?.includes('["flow-children","onActivate","onCancel","focusClassName",')) return m[prop]; } -}) as VFC>; \ No newline at end of file +}) as VFC>; diff --git a/src/deck-components/FooterLegend.ts b/src/deck-components/FooterLegend.ts index 2519f33..0bb09b4 100644 --- a/src/deck-components/FooterLegend.ts +++ b/src/deck-components/FooterLegend.ts @@ -1,66 +1,66 @@ export enum GamepadButton { - INVALID, - OK, - CANCEL, - SECONDARY, - OPTIONS, - BUMPER_LEFT, - BUMPER_RIGHT, - TRIGGER_LEFT, - TRIGGER_RIGHT, - DIR_UP, - DIR_DOWN, - DIR_LEFT, - DIR_RIGHT, - SELECT, - START, - LSTICK_CLICK, - RSTICK_CLICK, - LSTICK_TOUCH, - RSTICK_TOUCH, - LPAD_TOUCH, - LPAD_CLICK, - RPAD_TOUCH, - RPAD_CLICK, - REAR_LEFT_UPPER, - REAR_LEFT_LOWER, - REAR_RIGHT_UPPER, - REAR_RIGHT_LOWER, - STEAM_GUIDE, - STEAM_QUICK_MENU + INVALID, + OK, + CANCEL, + SECONDARY, + OPTIONS, + BUMPER_LEFT, + BUMPER_RIGHT, + TRIGGER_LEFT, + TRIGGER_RIGHT, + DIR_UP, + DIR_DOWN, + DIR_LEFT, + DIR_RIGHT, + SELECT, + START, + LSTICK_CLICK, + RSTICK_CLICK, + LSTICK_TOUCH, + RSTICK_TOUCH, + LPAD_TOUCH, + LPAD_CLICK, + RPAD_TOUCH, + RPAD_CLICK, + REAR_LEFT_UPPER, + REAR_LEFT_LOWER, + REAR_RIGHT_UPPER, + REAR_RIGHT_LOWER, + STEAM_GUIDE, + STEAM_QUICK_MENU, } export enum NavEntryPositionPreferences { - FIRST, - LAST, - MAINTAIN_X, - MAINTAIN_Y, - PREFERRED_CHILD + FIRST, + LAST, + MAINTAIN_X, + MAINTAIN_Y, + PREFERRED_CHILD, } export interface GamepadEventDetail { - button: number; - is_repeat?: boolean; - source: number; + button: number; + is_repeat?: boolean; + source: number; } -export type GamepadEvent = CustomEvent +export type GamepadEvent = CustomEvent; export interface FooterLegendProps { - actionDescriptionMap?: unknown; - onOKActionDescription?: string; - onCancelActionDescription?: string; - onSecondaryActionDescription?: string; - onOptionsActionDescription?: string; - onMenuActionDescription?: string; - onButtonDown?: (evt: GamepadEvent) => void; - onButtonUp?: (evt: GamepadEvent) => void; - onOKButton?: (evt: GamepadEvent) => void; - onCancelButton?: (evt: GamepadEvent) => void; - onSecondaryButton?: (evt: GamepadEvent) => void; - onOptionsButton?: (evt: GamepadEvent) => void; - onGamepadDirection?: (evt: GamepadEvent) => void; - onGamepadFocus?: (evt: GamepadEvent) => void; - onGamepadBlur?: (evt: GamepadEvent) => void; - onMenuButton?: (evt: GamepadEvent) => void; -} \ No newline at end of file + actionDescriptionMap?: unknown; + onOKActionDescription?: string; + onCancelActionDescription?: string; + onSecondaryActionDescription?: string; + onOptionsActionDescription?: string; + onMenuActionDescription?: string; + onButtonDown?: (evt: GamepadEvent) => void; + onButtonUp?: (evt: GamepadEvent) => void; + onOKButton?: (evt: GamepadEvent) => void; + onCancelButton?: (evt: GamepadEvent) => void; + onSecondaryButton?: (evt: GamepadEvent) => void; + onOptionsButton?: (evt: GamepadEvent) => void; + onGamepadDirection?: (evt: GamepadEvent) => void; + onGamepadFocus?: (evt: GamepadEvent) => void; + onGamepadBlur?: (evt: GamepadEvent) => void; + onMenuButton?: (evt: GamepadEvent) => void; +} diff --git a/src/deck-components/Modal.tsx b/src/deck-components/Modal.tsx index e82c409..ed1e1ae 100755 --- a/src/deck-components/Modal.tsx +++ b/src/deck-components/Modal.tsx @@ -1,4 +1,5 @@ import { FC, ReactNode } from 'react'; + import { findSP } from '../utils'; import { findModuleChild } from '../webpack'; @@ -21,25 +22,26 @@ export interface ShowModalResult { 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: + // 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: // { console.log("ABOUT TO CLOSE"); showModalRes.Close(); }} /> Update: (modal: ReactNode) => void; } -const showModalRaw: (modal: ReactNode, parent?: EventTarget, props?: ShowModalProps) => Promise = 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]; +const showModalRaw: (modal: ReactNode, parent?: EventTarget, props?: ShowModalProps) => Promise = + 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): Promise => { - return showModalRaw(modal, parent || findSP(), props) -} + return showModalRaw(modal, parent || findSP(), props); +}; export interface ModalRootProps { children?: ReactNode; diff --git a/src/deck-components/ProgressBar.tsx b/src/deck-components/ProgressBar.tsx index 61623cc..6dcc9bf 100644 --- a/src/deck-components/ProgressBar.tsx +++ b/src/deck-components/ProgressBar.tsx @@ -1,4 +1,4 @@ -import { VFC, ReactNode } from 'react'; +import { ReactNode, VFC } from 'react'; import { findModuleChild } from '../webpack'; import { ItemProps } from './Item'; diff --git a/src/deck-components/Router.tsx b/src/deck-components/Router.tsx index 1b7b696..53c7706 100644 --- a/src/deck-components/Router.tsx +++ b/src/deck-components/Router.tsx @@ -57,11 +57,11 @@ export enum DisplayStatus { } export type AppOverview = { - appid: string - display_name: string - display_status: DisplayStatus - sort_as: string -} + appid: string; + display_name: string; + display_status: DisplayStatus; + sort_as: string; +}; export interface Router { CloseSideMenus(): void; @@ -69,19 +69,19 @@ export interface Router { GetQuickAccessTab(): QuickAccessTab; Navigate(path: string): void; NavigateBackOrOpenMenu(): void; - NavigateToAppProperties(): void - NavigateToBugForum(): void + NavigateToAppProperties(): void; + NavigateToBugForum(): void; NavigateToExternalWeb(url: string): void; - NavigateToHelp(): void - NavigateToInvites(): void + NavigateToHelp(): void; + NavigateToInvites(): void; NavigateToRunningApp(replace?: boolean): void; - NavigateToStorage(): void - NavigateToStore(): void - NavigateToStoreApp(appId: number | string): void - NavigateToStoreFreeToPlay(): void - NavigateToStoreManual(): void - NavigateToStoreNewReleases(): void - NavigateToStoreOnSale(): void + NavigateToStorage(): void; + NavigateToStore(): void; + NavigateToStoreApp(appId: number | string): void; + NavigateToStoreFreeToPlay(): void; + NavigateToStoreManual(): void; + NavigateToStoreNewReleases(): void; + NavigateToStoreOnSale(): void; ToggleSideMenu(sideMenu: SideMenu): void; CloseSideMenus(): void; OpenSideMenu(sideMenu: SideMenu): void; diff --git a/src/deck-components/SidebarNavigation.tsx b/src/deck-components/SidebarNavigation.tsx index 85228cc..8afe94b 100644 --- a/src/deck-components/SidebarNavigation.tsx +++ b/src/deck-components/SidebarNavigation.tsx @@ -11,7 +11,7 @@ export interface SidebarNavigationPage { identifier?: string; route?: string; link?: string; - padding?: "none" | "compact"; + padding?: 'none' | 'compact'; } export interface SidebarNavigationProps { diff --git a/src/deck-components/Spinner.tsx b/src/deck-components/Spinner.tsx index 7fa76a0..ec3d688 100755 --- a/src/deck-components/Spinner.tsx +++ b/src/deck-components/Spinner.tsx @@ -3,6 +3,6 @@ import { FC, SVGAttributes } from 'react'; import { IconsModule } from '../webpack'; // TODO type this and other icons? -export const Spinner = Object.values(IconsModule).find((mod: any) => - mod?.toString && /Spinner\)}\),.\.createElement\(\"path\",{d:\"M18 /.test(mod.toString()) -) as FC>; \ No newline at end of file +export const Spinner = Object.values(IconsModule).find( + (mod: any) => mod?.toString && /Spinner\)}\),.\.createElement\(\"path\",{d:\"M18 /.test(mod.toString()), +) as FC>; diff --git a/src/deck-components/SteamSpinner.tsx b/src/deck-components/SteamSpinner.tsx index b758ed2..646a259 100755 --- a/src/deck-components/SteamSpinner.tsx +++ b/src/deck-components/SteamSpinner.tsx @@ -1,9 +1,11 @@ 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("PreloadThrobber")) return m[prop] - } -}) as FC>; \ No newline at end of file + if (typeof m !== 'object') return undefined; + for (let prop in m) { + if (m[prop]?.toString()?.includes('Steam Spinner') && m[prop].toString().includes('PreloadThrobber')) + return m[prop]; + } +}) as FC>; diff --git a/src/deck-components/static-classes.ts b/src/deck-components/static-classes.ts index 7e102e5..e145a12 100644 --- a/src/deck-components/static-classes.ts +++ b/src/deck-components/static-classes.ts @@ -63,13 +63,7 @@ type QuickAccessMenuClasses = Record< string >; -type ScrollPanelClasses = Record< - | 'ScrollBoth' - | 'ScrollPanel' - | 'ScrollX' - | 'ScrollY', - string ->; +type ScrollPanelClasses = Record<'ScrollBoth' | 'ScrollPanel' | 'ScrollX' | 'ScrollY', string>; type GamepadDialogClasses = Record< | 'duration-app-launch' @@ -166,207 +160,221 @@ type QuickAccessControlsClasses = Record< >; type UpdaterFieldClasses = Record< - | "duration-app-launch" - | "OOBEUpdateStatusContainer" - | "UpdateScreen" - | "UpdatePanel" - | "CurrentStatus" - | "TotalUpdateSize" - | "ProgressInfoContainer" - | "TimeRemaining" - | "BatteryLowWarning" - | "fadeInAnimation" - | "ProgressStatus" - | "UpdateStatusContainer" - | "UpdaterFieldStatusSuccess" - | "UpdaterFieldStatusApplying" - | "TextContainer" - | "ApplyingText" - | "UpdateBytesRemaining" - | "Label" - | "Numerator" - | "Separator" - | "Denominator" - | "PatchNotes" - | "PostedTime" - | "EventDetailTitle" - | "EventDetailsSubTitle" - | "EventDetailsBody" - | "InsufficientBatteryText" - | "UnsupportedHardwareWarning" - | "Title" - | "Text" - | "Body" - | "ItemFocusAnim-darkerGrey-nocolor" - | "ItemFocusAnim-darkerGrey" - | "ItemFocusAnim-darkGrey" - | "ItemFocusAnim-grey" - | "ItemFocusAnimBorder-darkGrey" - | "ItemFocusAnim-green" - | "focusAnimation" - | "hoverAnimation", + | 'duration-app-launch' + | 'OOBEUpdateStatusContainer' + | 'UpdateScreen' + | 'UpdatePanel' + | 'CurrentStatus' + | 'TotalUpdateSize' + | 'ProgressInfoContainer' + | 'TimeRemaining' + | 'BatteryLowWarning' + | 'fadeInAnimation' + | 'ProgressStatus' + | 'UpdateStatusContainer' + | 'UpdaterFieldStatusSuccess' + | 'UpdaterFieldStatusApplying' + | 'TextContainer' + | 'ApplyingText' + | 'UpdateBytesRemaining' + | 'Label' + | 'Numerator' + | 'Separator' + | 'Denominator' + | 'PatchNotes' + | 'PostedTime' + | 'EventDetailTitle' + | 'EventDetailsSubTitle' + | 'EventDetailsBody' + | 'InsufficientBatteryText' + | 'UnsupportedHardwareWarning' + | 'Title' + | 'Text' + | 'Body' + | 'ItemFocusAnim-darkerGrey-nocolor' + | 'ItemFocusAnim-darkerGrey' + | 'ItemFocusAnim-darkGrey' + | 'ItemFocusAnim-grey' + | 'ItemFocusAnimBorder-darkGrey' + | 'ItemFocusAnim-green' + | 'focusAnimation' + | 'hoverAnimation', string >; type PlaySectionClasses = Record< - | "AchievementCountLabel" - | "AchievementProgressRow" - | "ActionSection" - | "AppButtonsContainer" - | "Arrow" - | "AvatarAndPersona" - | "BreakNarrow" - | "BreakShort" - | "BreakTall" - | "BreakUltraWide" - | "BreakWide" - | "ClickablePlayBarItem" - | "CloudStatusIcon" - | "CloudStatusLabel" - | "CloudStatusRow" - | "CloudSyncProblem" - | "CloudSynching" - | "ComingSoon" - | "Container" - | "DetailsProgressBar" - | "DetailsProgressContainer" - | "DetailsSection" - | "DetailsSectionExtra" - | "DetailsSectionStatus" - | "DotDotDot" - | "DownloadPaused" - | "DownloadProgressBar" - | "Downloading" - | "FavoriteButton" - | "Favorited" - | "GameInfoButton" - | "GameStat" - | "GameStatIcon" - | "GameStatIconForced" - | "GameStatRight" - | "GameStatsSection" - | "GamepadUIBreakNarrow" - | "GamepadUIBreakShort" - | "GamepadUIBreakWide" - | "Glassy" - | "HideWhenNarrow" - | "Icon" - | "Icons" - | "InPage" - | "InnerContainer" - | "InvalidPlatform" - | "ItemFocusAnim-darkGrey" - | "ItemFocusAnim-darkerGrey" - | "ItemFocusAnim-darkerGrey-nocolor" - | "ItemFocusAnim-green" - | "ItemFocusAnim-grey" - | "ItemFocusAnimBorder-darkGrey" - | "Label" - | "LastPlayed" - | "LastPlayedInfo" - | "MenuActive" - | "MenuButton" - | "MiniAchievements" - | "OfflineMode" - | "OnlyDownloadBar" - | "PermanentlyUnavailable" - | "PlayBar" - | "PlayBarCloudStatusContainer" - | "PlayBarDetailLabel" - | "PlayBarGameIcon" - | "PlayBarGameName" - | "PlayBarIconAndGame" - | "PlayBarLabel" - | "Playtime" - | "PlaytimeIcon" - | "PlaytimeIconForced" - | "PortraitBar" - | "Presale" - | "RecentlyUpdated" - | "RecentlyUpdatedIcon" - | "RecentlyUpdatedLink" - | "RecentlyUpdatedText" - | "RightBreakNarrow" - | "RightBreakUltraNarrow" - | "RightBreakUltraWide" - | "RightBreakWide" - | "RightControls" - | "Row" - | "SharedLibrary" - | "StatusAndStats" - | "StatusNameContainer" - | "StickyHeader" - | "StickyHeaderShadow" - | "SuperimposedGridItems" - | "SyncAnim" - | "Visible" - | "duration-app-launch" - | "favorited" - | "focusAnimation" - | "hoverAnimation", + | 'AchievementCountLabel' + | 'AchievementProgressRow' + | 'ActionSection' + | 'AppButtonsContainer' + | 'Arrow' + | 'AvatarAndPersona' + | 'BreakNarrow' + | 'BreakShort' + | 'BreakTall' + | 'BreakUltraWide' + | 'BreakWide' + | 'ClickablePlayBarItem' + | 'CloudStatusIcon' + | 'CloudStatusLabel' + | 'CloudStatusRow' + | 'CloudSyncProblem' + | 'CloudSynching' + | 'ComingSoon' + | 'Container' + | 'DetailsProgressBar' + | 'DetailsProgressContainer' + | 'DetailsSection' + | 'DetailsSectionExtra' + | 'DetailsSectionStatus' + | 'DotDotDot' + | 'DownloadPaused' + | 'DownloadProgressBar' + | 'Downloading' + | 'FavoriteButton' + | 'Favorited' + | 'GameInfoButton' + | 'GameStat' + | 'GameStatIcon' + | 'GameStatIconForced' + | 'GameStatRight' + | 'GameStatsSection' + | 'GamepadUIBreakNarrow' + | 'GamepadUIBreakShort' + | 'GamepadUIBreakWide' + | 'Glassy' + | 'HideWhenNarrow' + | 'Icon' + | 'Icons' + | 'InPage' + | 'InnerContainer' + | 'InvalidPlatform' + | 'ItemFocusAnim-darkGrey' + | 'ItemFocusAnim-darkerGrey' + | 'ItemFocusAnim-darkerGrey-nocolor' + | 'ItemFocusAnim-green' + | 'ItemFocusAnim-grey' + | 'ItemFocusAnimBorder-darkGrey' + | 'Label' + | 'LastPlayed' + | 'LastPlayedInfo' + | 'MenuActive' + | 'MenuButton' + | 'MiniAchievements' + | 'OfflineMode' + | 'OnlyDownloadBar' + | 'PermanentlyUnavailable' + | 'PlayBar' + | 'PlayBarCloudStatusContainer' + | 'PlayBarDetailLabel' + | 'PlayBarGameIcon' + | 'PlayBarGameName' + | 'PlayBarIconAndGame' + | 'PlayBarLabel' + | 'Playtime' + | 'PlaytimeIcon' + | 'PlaytimeIconForced' + | 'PortraitBar' + | 'Presale' + | 'RecentlyUpdated' + | 'RecentlyUpdatedIcon' + | 'RecentlyUpdatedLink' + | 'RecentlyUpdatedText' + | 'RightBreakNarrow' + | 'RightBreakUltraNarrow' + | 'RightBreakUltraWide' + | 'RightBreakWide' + | 'RightControls' + | 'Row' + | 'SharedLibrary' + | 'StatusAndStats' + | 'StatusNameContainer' + | 'StickyHeader' + | 'StickyHeaderShadow' + | 'SuperimposedGridItems' + | 'SyncAnim' + | 'Visible' + | 'duration-app-launch' + | 'favorited' + | 'focusAnimation' + | 'hoverAnimation', string >; type GamepadSliderClasses = Record< - | "error-shake-duration" - | "SliderControlPanelGroup" - | "SliderControlAndNotches" - | "WithDefaultValue" - | "SliderControl" - | "Disabled" - | "SliderTrack" - | "SliderHasNotches" - | "SliderTrackDark" - | "SliderHandleContainer" - | "VerticalLineSliderHandleContainer" - | "ParenSliderHandleContainer" - | "SliderHandle" - | "SliderHandleFocusPop" - | "VerticalLineSliderHandle" - | "ParenSliderHandle" - | "Left" - | "SliderControlWithIcon" - | "Icon" - | "SliderNotchContainer" - | "SliderNotch" - | "AlignToEnds" - | "SliderNotchLabel" - | "AlignToLeft" - | "AlignToRight" - | "SliderNotchTick" - | "TickActive" - | "LabelText" - | "DescriptionValue" - | "EditableValue" - | "FakeEditableValue" - | "RedBorder" - | "EditableValueSuffix" - | "ErrorShake" - | "error-shake" - | "CompoundSlider" - | "CompoundSliderSubSlider" - | "Right" - | "CompoundSliderSubSliderLabelContainer" - | "CompoundSliderSubSliderLabelPositioner" - | "CompoundSliderSubSliderLabel" - | "CompoundSliderSubSliderLabelInternal" - | "DefaultValueTickContainer" - | "DefaultValueTick", + | 'error-shake-duration' + | 'SliderControlPanelGroup' + | 'SliderControlAndNotches' + | 'WithDefaultValue' + | 'SliderControl' + | 'Disabled' + | 'SliderTrack' + | 'SliderHasNotches' + | 'SliderTrackDark' + | 'SliderHandleContainer' + | 'VerticalLineSliderHandleContainer' + | 'ParenSliderHandleContainer' + | 'SliderHandle' + | 'SliderHandleFocusPop' + | 'VerticalLineSliderHandle' + | 'ParenSliderHandle' + | 'Left' + | 'SliderControlWithIcon' + | 'Icon' + | 'SliderNotchContainer' + | 'SliderNotch' + | 'AlignToEnds' + | 'SliderNotchLabel' + | 'AlignToLeft' + | 'AlignToRight' + | 'SliderNotchTick' + | 'TickActive' + | 'LabelText' + | 'DescriptionValue' + | 'EditableValue' + | 'FakeEditableValue' + | 'RedBorder' + | 'EditableValueSuffix' + | 'ErrorShake' + | 'error-shake' + | 'CompoundSlider' + | 'CompoundSliderSubSlider' + | 'Right' + | 'CompoundSliderSubSliderLabelContainer' + | 'CompoundSliderSubSliderLabelPositioner' + | 'CompoundSliderSubSliderLabel' + | 'CompoundSliderSubSliderLabelInternal' + | 'DefaultValueTickContainer' + | 'DefaultValueTick', string >; -export const quickAccessMenuClasses: QuickAccessMenuClasses = findModule((mod) => typeof mod === 'object' && mod?.Title?.includes('quickaccessmenu')); +export const quickAccessMenuClasses: QuickAccessMenuClasses = findModule( + (mod) => typeof mod === 'object' && mod?.Title?.includes('quickaccessmenu'), +); /** * @depreciated please use quickAccessMenuClasses instead */ export const staticClasses = quickAccessMenuClasses; -export const scrollPanelClasses: ScrollPanelClasses = findModule((mod) => typeof mod === 'object' && mod?.ScrollPanel?.includes('scrollpanel')); +export const scrollPanelClasses: ScrollPanelClasses = findModule( + (mod) => typeof mod === 'object' && mod?.ScrollPanel?.includes('scrollpanel'), +); /** * @depreciated please use scrollPanelClasses instead */ export const scrollClasses = scrollPanelClasses; -export const gamepadDialogClasses: GamepadDialogClasses = findModule((mod) => typeof mod === 'object' && mod?.GamepadDialogContent?.includes('gamepaddialog')); -export const quickAccessControlsClasses: QuickAccessControlsClasses = findModule((mod) => typeof mod === 'object' && mod?.PanelSection?.includes('quickaccesscontrols')); -export const updaterFieldClasses: UpdaterFieldClasses = findModule((mod) => typeof mod === 'object' && mod?.OOBEUpdateStatusContainer?.includes('updaterfield')); -export const playSectionClasses: PlaySectionClasses = findModule((mod) => typeof mod === 'object' && mod?.Container?.includes('appdetailsplaysection')); -export const gamepadSliderClasses: GamepadSliderClasses = findModule((mod) => typeof mod === 'object' && mod?.SliderControlPanelGroup?.includes('gamepadslider')); \ No newline at end of file +export const gamepadDialogClasses: GamepadDialogClasses = findModule( + (mod) => typeof mod === 'object' && mod?.GamepadDialogContent?.includes('gamepaddialog'), +); +export const quickAccessControlsClasses: QuickAccessControlsClasses = findModule( + (mod) => typeof mod === 'object' && mod?.PanelSection?.includes('quickaccesscontrols'), +); +export const updaterFieldClasses: UpdaterFieldClasses = findModule( + (mod) => typeof mod === 'object' && mod?.OOBEUpdateStatusContainer?.includes('updaterfield'), +); +export const playSectionClasses: PlaySectionClasses = findModule( + (mod) => typeof mod === 'object' && mod?.Container?.includes('appdetailsplaysection'), +); +export const gamepadSliderClasses: GamepadSliderClasses = findModule( + (mod) => typeof mod === 'object' && mod?.SliderControlPanelGroup?.includes('gamepadslider'), +); diff --git a/src/deck-hooks/index.ts b/src/deck-hooks/index.ts index 1009bae..15663ba 100644 --- a/src/deck-hooks/index.ts +++ b/src/deck-hooks/index.ts @@ -1 +1 @@ -export * from './useParams' +export * from './useParams'; diff --git a/src/deck-hooks/useParams.ts b/src/deck-hooks/useParams.ts index 39f7fe3..1434c15 100644 --- a/src/deck-hooks/useParams.ts +++ b/src/deck-hooks/useParams.ts @@ -1,15 +1,15 @@ -import { ReactRouter } from "../webpack"; +import { ReactRouter } from '../webpack'; /** * Get the current params from ReactRouter - * + * * @returns an object with the current ReactRouter params - * + * * @example * import { useParams } from "decky-frontend-lib"; - * + * * const { appid } = useParams<{ appid: string }>() */ -export const useParams = Object.values(ReactRouter).find((val) => - /return (\w)\?\1\.params:{}/.test(`${val}`) -) as () => T \ No newline at end of file +export const useParams = Object.values(ReactRouter).find((val) => /return (\w)\?\1\.params:{}/.test(`${val}`)) as < + T, +>() => T; diff --git a/src/index.ts b/src/index.ts index 0d36125..2ebe40e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ export * from './custom-components'; export * from './custom-hooks'; export * from './deck-components'; -export * from './deck-hooks' +export * from './deck-hooks'; export * from './plugin'; export * from './webpack'; export * from './utils'; diff --git a/src/plugin.tsx b/src/plugin.tsx index 04526dc..e2e6ed7 100644 --- a/src/plugin.tsx +++ b/src/plugin.tsx @@ -54,7 +54,7 @@ export interface FilePickerRes { export interface ServerAPI { routerHook: RouterHook; toaster: Toaster; - openFilePicker(startPath: string, includeFiles?: boolean, regex?: RegExp): Promise + openFilePicker(startPath: string, includeFiles?: boolean, regex?: RegExp): Promise; callPluginMethod(methodName: string, args: TArgs): Promise>; callServerMethod(methodName: string, args: TArgs): Promise>; fetchNoCors(url: RequestInfo, request?: RequestInit): Promise>; diff --git a/src/utils/index.ts b/src/utils/index.ts index 7727975..f1f6b96 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,22 +1,21 @@ -export * from "./patcher"; -export * from "./react"; +export * from './patcher'; +export * from './react'; export function joinClassNames(...classes: string[]): string { - return classes.join(" "); + return classes.join(' '); } export function sleep(ms: number) { - return new Promise(res => setTimeout(res, ms)); + return new Promise((res) => setTimeout(res, ms)); } /** * Finds the SP window, since it is a render target as of {10-19-2022}'s beta */ 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; - } - \ No newline at end of file + // 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; +} diff --git a/src/utils/patcher.ts b/src/utils/patcher.ts index 0e6bdf0..0e6c41d 100644 --- a/src/utils/patcher.ts +++ b/src/utils/patcher.ts @@ -1,112 +1,160 @@ // TODO: implement storing patches as an option so we can offer unpatchAll selectively // Return this in a replacePatch to call the original method (can still modify args). -export let callOriginal = Symbol("DECKY_CALL_ORIGINAL"); +export let callOriginal = Symbol('DECKY_CALL_ORIGINAL'); export interface PatchOptions { - singleShot?: boolean + singleShot?: boolean; } type GenericPatchHandler = (args: any[], ret?: any) => any; export interface Patch { - original: Function; - property: string; - object: any; - patchedFunction: any; - hasUnpatched: boolean; - handler: GenericPatchHandler; + original: Function; + property: string; + object: any; + patchedFunction: any; + hasUnpatched: boolean; + handler: GenericPatchHandler; - unpatch: () => void -}; + unpatch: () => void; +} // let patches = new Set(); -export function beforePatch(object: any, property: string, handler: (args: any[]) => any, options: PatchOptions = {}): Patch { - const orig = object[property]; - object[property] = function (...args: any[]) { - handler.call(this, args); - const ret = patch.original.call(this, ...args); - if (options.singleShot) { - patch.unpatch(); - } - return ret; +export function beforePatch( + object: any, + property: string, + handler: (args: any[]) => any, + options: PatchOptions = {}, +): Patch { + const orig = object[property]; + object[property] = function (...args: any[]) { + handler.call(this, args); + const ret = patch.original.call(this, ...args); + if (options.singleShot) { + patch.unpatch(); } - const patch = processPatch(object, property, handler, object[property], orig); - return patch; + return ret; + }; + const patch = processPatch(object, property, handler, object[property], orig); + return patch; } -export function afterPatch(object: any, property: string, handler: (args: any[], ret: any) => any, options: PatchOptions = {}): Patch { - const orig = object[property]; - object[property] = function (...args: any[]) { - let ret = patch.original.call(this, ...args); - ret = handler.call(this, args, ret); - if (options.singleShot) { - patch.unpatch(); - } - return ret; +export function afterPatch( + object: any, + property: string, + handler: (args: any[], ret: any) => any, + options: PatchOptions = {}, +): Patch { + const orig = object[property]; + object[property] = function (...args: any[]) { + let ret = patch.original.call(this, ...args); + ret = handler.call(this, args, ret); + if (options.singleShot) { + patch.unpatch(); } - const patch = processPatch(object, property, handler, object[property], orig); - return patch; + return ret; + }; + const patch = processPatch(object, property, handler, object[property], orig); + return patch; } -export function replacePatch(object: any, property: string, handler: (args: any[]) => any, options: PatchOptions = {}): Patch { - const orig = object[property]; - object[property] = function (...args: any[]) { - const ret = handler.call(this, args); - if (ret == callOriginal) return patch.original.call(this, ...args); - if (options.singleShot) { - patch.unpatch(); - } - return ret; - }; - const patch = processPatch(object, property, handler, object[property], orig); - return patch; +export function replacePatch( + object: any, + property: string, + handler: (args: any[]) => any, + options: PatchOptions = {}, +): Patch { + const orig = object[property]; + object[property] = function (...args: any[]) { + const ret = handler.call(this, args); + if (ret == callOriginal) return patch.original.call(this, ...args); + if (options.singleShot) { + patch.unpatch(); + } + return ret; + }; + const patch = processPatch(object, property, handler, object[property], orig); + return patch; } -function processPatch(object: any, property: any, handler: GenericPatchHandler, patchedFunction: any, original: any): Patch { - // Assign all props of original function to new one - Object.assign(object[property], original); - // Allow toString webpack filters to continue to work - object[property].toString = () => original.toString(); +function processPatch( + object: any, + property: any, + handler: GenericPatchHandler, + patchedFunction: any, + original: any, +): Patch { + // Assign all props of original function to new one + Object.assign(object[property], original); + // Allow toString webpack filters to continue to work + object[property].toString = () => original.toString(); - // HACK: for compatibility, remove when all plugins are using new patcher - Object.defineProperty(object[property], "__deckyOrig", { - get: () => patch.original, - set: (val: any) => patch.original = val - }) + // HACK: for compatibility, remove when all plugins are using new patcher + Object.defineProperty(object[property], '__deckyOrig', { + get: () => patch.original, + set: (val: any) => (patch.original = val), + }); - // Build a Patch object of this patch - const patch: Patch = { - object, - property, - handler, - patchedFunction, - original, - hasUnpatched: false, - unpatch: () => unpatch(patch) - }; + // Build a Patch object of this patch + const patch: Patch = { + object, + property, + handler, + patchedFunction, + original, + hasUnpatched: false, + unpatch: () => unpatch(patch), + }; - object[property].__deckyPatch = patch; + object[property].__deckyPatch = patch; - return patch; + return patch; } function unpatch(patch: Patch): void { - const { object, property, handler, patchedFunction, original } = patch; - if (patch.hasUnpatched) throw new Error("Function is already unpatched.") - let realProp = property; - let realObject = object; - console.debug("[Patcher] unpatching", {realObject, realProp, object, property, handler, patchedFunction, original, isEqual: realObject[realProp] === patchedFunction}) + const { object, property, handler, patchedFunction, original } = patch; + if (patch.hasUnpatched) throw new Error('Function is already unpatched.'); + let realProp = property; + let realObject = object; + console.debug('[Patcher] unpatching', { + realObject, + realProp, + object, + property, + handler, + patchedFunction, + original, + isEqual: realObject[realProp] === patchedFunction, + }); - // If another patch has been applied to this function after this one, move down until we find the correct patch - while (realObject[realProp] && realObject[realProp] !== patchedFunction) { - realObject = realObject[realProp].__deckyPatch; - realProp = "original"; - console.debug("[Patcher] moved to next", {realObject, realProp, object, property, handler, patchedFunction, original, isEqual: realObject[realProp] === patchedFunction}) - } + // If another patch has been applied to this function after this one, move down until we find the correct patch + while (realObject[realProp] && realObject[realProp] !== patchedFunction) { + realObject = realObject[realProp].__deckyPatch; + realProp = 'original'; + console.debug('[Patcher] moved to next', { + realObject, + realProp, + object, + property, + handler, + patchedFunction, + original, + isEqual: realObject[realProp] === patchedFunction, + }); + } - realObject[realProp] = realObject[realProp].__deckyPatch.original + realObject[realProp] = realObject[realProp].__deckyPatch.original; - patch.hasUnpatched = true; - console.debug("[Patcher] unpatched", {realObject, realProp, object, property, handler, patchedFunction, original, isEqual: realObject[realProp] === patchedFunction}) -} \ No newline at end of file + patch.hasUnpatched = true; + console.debug('[Patcher] unpatched', { + realObject, + realProp, + object, + property, + handler, + patchedFunction, + original, + isEqual: realObject[realProp] === patchedFunction, + }); +} diff --git a/src/utils/react.ts b/src/utils/react.ts index cf84180..2971364 100644 --- a/src/utils/react.ts +++ b/src/utils/react.ts @@ -1,86 +1,95 @@ -import * as React from "react"; +import * as React from 'react'; // this shouldn't need to be redeclared but it does for some reason declare global { - interface Window { - SP_REACT: typeof React; - } + interface Window { + SP_REACT: typeof React; + } } -export function fakeRenderComponent(fun: Function): any { - const hooks = (window.SP_REACT as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current; +export function fakeRenderComponent(fun: Function, customHooks: any = {}): any { + const hooks = (window.SP_REACT as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher + .current; - // TODO: add more hooks + // TODO: add more hooks - let oldHooks = { - useContext: hooks.useContext, - useCallback: hooks.useCallback, - useLayoutEffect: hooks.useLayoutEffect, - useEffect: hooks.useEffect, - useMemo: hooks.useMemo, - useRef: hooks.useRef, - useState: hooks.useState, - } + let oldHooks = { + useContext: hooks.useContext, + useCallback: hooks.useCallback, + useLayoutEffect: hooks.useLayoutEffect, + useEffect: hooks.useEffect, + useMemo: hooks.useMemo, + useRef: hooks.useRef, + useState: hooks.useState, + }; - hooks.useCallback = (cb: Function) => cb; - hooks.useContext = (cb: any) => cb._currentValue; - hooks.useLayoutEffect = (_: Function) => {}//cb(); - hooks.useMemo = (cb: Function, _: any[]) => cb; - hooks.useEffect = (_: Function) => {}//cb(); - hooks.useRef = (val: any) => ({current: val || {}}); - hooks.useState = (v: any) => { - let val = v; + hooks.useCallback = (cb: Function) => cb; + hooks.useContext = (cb: any) => cb._currentValue; + hooks.useLayoutEffect = (_: Function) => {}; //cb(); + hooks.useMemo = (cb: Function, _: any[]) => cb; + hooks.useEffect = (_: Function) => {}; //cb(); + hooks.useRef = (val: any) => ({ current: val || {} }); + hooks.useState = (v: any) => { + let val = v; - return [val, (n: any) => val = n]; - }; + return [val, (n: any) => (val = n)]; + }; - const res = fun(hooks); + Object.assign(hooks, customHooks); - Object.assign(hooks, oldHooks); + const res = fun(hooks); - return res; + Object.assign(hooks, oldHooks); + + return res; } export function wrapReactType(node: any, prop: any = 'type') { - return node[prop] = {...node[prop]}; + return (node[prop] = { ...node[prop] }); } export function wrapReactClass(node: any, prop: any = 'type') { - const cls = node[prop]; - const wrappedCls = class extends cls {}; - return node[prop] = wrappedCls; + const cls = node[prop]; + const wrappedCls = class extends cls {}; + return (node[prop] = wrappedCls); } export function getReactInstance(o: HTMLElement | Element | Node) { - return o[Object.keys(o).find(k => k.startsWith('__reactInternalInstance')) as string] + return o[Object.keys(o).find((k) => k.startsWith('__reactInternalInstance')) as string]; } // Based on https://github.com/GooseMod/GooseMod/blob/9ef146515a9e59ed4e25665ed365fd72fc0dcf23/src/util/react.js#L20 export interface findInTreeOpts { - walkable?: string[], - ignore?: string[] + walkable?: string[]; + ignore?: string[]; } -export declare type findInTreeFilter = (element: any) => boolean +export declare type findInTreeFilter = (element: any) => boolean; export const findInTree = (parent: any, filter: findInTreeFilter, opts: findInTreeOpts): any => { - const { walkable = null, ignore = [] } = opts ?? {}; - - if (!parent || typeof parent !== 'object') { // Parent is invalid to search through - return null; - } - - if (filter(parent)) return parent; // Parent matches, just return - - if (Array.isArray(parent)) { // Parent is an array, go through values - return parent.map((x) => findInTree(x, filter, opts)).find((x) => x); - } - - // Parent is an object, go through values (or option to only use certain keys) - return (walkable || Object.keys(parent)).map((x) => !ignore.includes(x) && findInTree(parent[x], filter, opts)).find((x: any) => x); + const { walkable = null, ignore = [] } = opts ?? {}; + + if (!parent || typeof parent !== 'object') { + // Parent is invalid to search through + return null; + } + + if (filter(parent)) return parent; // Parent matches, just return + + if (Array.isArray(parent)) { + // Parent is an array, go through values + return parent.map((x) => findInTree(x, filter, opts)).find((x) => x); + } + + // Parent is an object, go through values (or option to only use certain keys) + return (walkable || Object.keys(parent)) + .map((x) => !ignore.includes(x) && findInTree(parent[x], filter, opts)) + .find((x: any) => x); }; - -export const findInReactTree = (node: any, filter: findInTreeFilter) => findInTree(node, filter, { // Specialised findInTree for React nodes - walkable: [ 'props', 'children', 'child', 'sibling' ] -}); \ No newline at end of file + +export const findInReactTree = (node: any, filter: findInTreeFilter) => + findInTree(node, filter, { + // Specialised findInTree for React nodes + walkable: ['props', 'children', 'child', 'sibling'], + }); diff --git a/src/webpack.ts b/src/webpack.ts index 73a5aac..bc75c19 100644 --- a/src/webpack.ts +++ b/src/webpack.ts @@ -29,15 +29,23 @@ if (window.webpackJsonp && !window.webpackJsonp.deckyShimmed) { hasWebpack5 = true; const id = Math.random(); let initReq: any; - window.webpackChunksteamui.push([[ id ], {}, (r: any) => { initReq = r }]); + window.webpackChunksteamui.push([ + [id], + {}, + (r: any) => { + initReq = r; + }, + ]); for (let i of Object.keys(initReq.m)) { - webpackCache[i] = initReq(i) + webpackCache[i] = initReq(i); } } -export const allModules: Module[] = hasWebpack5 ? Object.values(webpackCache).filter((x) => x) : Object.keys(webpackCache) - .map((x) => webpackCache[x].exports) - .filter((x) => x); +export const allModules: Module[] = hasWebpack5 + ? Object.values(webpackCache).filter((x) => x) + : Object.keys(webpackCache) + .map((x) => webpackCache[x].exports) + .filter((x) => x); export const findModule = (filter: FilterFn) => { for (const m of allModules) {