Compare commits

..

21 Commits

Author SHA1 Message Date
semantic-release-bot
4cdcca0b5a chore(release): 3.7.4 [CI SKIP] 2022-10-26 00:16:38 +00:00
AAGaming
f16e0b29f8 fix(tabs): fix on stable 2022-10-25 20:16:06 -04:00
AAGaming
37a6658b95 chore(docs): how did i manage to do this 2022-10-24 20:45:01 -04:00
AAGaming
ed0b92de2e fix(docs): change arg format 2022-10-24 20:43:53 -04:00
AAGaming
dcba5c22f8 chore(docs): fix it 2022-10-24 20:42:47 -04:00
semantic-release-bot
fa50ca6a37 chore(release): 3.7.3 [CI SKIP] 2022-10-25 00:38:49 +00:00
AAGaming
19e986ed8b chore(ci): switch to npm 2022-10-24 20:38:18 -04:00
AAGaming
3c553a227d fix(tabs): it returns 2022-10-24 20:35:01 -04:00
AAGaming
1f2694aec8 chore(prettier): fix prettier 2022-10-24 20:33:40 -04:00
semantic-release-bot
2e52cca8a2 chore(release): 3.7.2 [CI SKIP] 2022-10-24 14:59:05 +00:00
AAGaming
3dbca1a056 fix(tabs): unkill build 2022-10-24 10:58:35 -04:00
semantic-release-bot
c6692138c6 chore(release): 3.7.1 [CI SKIP] 2022-10-24 05:01:13 +00:00
AAGaming
25c33b2a05 fix(Tabs): temp remove until we have a way to grab it on beta 2022-10-24 01:00:44 -04:00
semantic-release-bot
00d27d1373 chore(release): 3.7.0 [CI SKIP] 2022-10-24 00:22:31 +00:00
AAGaming
5f0470c351 feat(modal): support for latest steamos preview 2022-10-23 20:22:04 -04:00
semantic-release-bot
c77d6edaae chore(release): 3.6.1 [CI SKIP] 2022-10-19 19:57:14 +00:00
Lukas Senionis
c44c66facd fix(plugin): export RoutePatch (#39) 2022-10-19 15:44:13 -04:00
semantic-release-bot
276e4eccd2 chore(release): 3.6.0 [CI SKIP] 2022-10-15 03:44:35 +00:00
AAGaming
2fc2060a6c feat(plugin): add alwaysRender 2022-10-14 23:43:45 -04:00
semantic-release-bot
1143a9f3e0 chore(release): 3.5.6 [CI SKIP] 2022-10-08 12:57:07 +00:00
Lukas Senionis
5a5218a7c4 fix(Dialog): remove not exported dialog button (#37) 2022-10-08 08:56:14 -04:00
34 changed files with 6094 additions and 23151 deletions

View File

@@ -29,12 +29,13 @@ jobs:
- name: Setup | Dependencies
run: |
cd lib
npm ci
npm i -g pnpm
pnpm i --frozen-lockfile
- name: Build Docs
run: |
cd lib
npm run docs -- --out ../wiki/api-docs/decky-frontend-lib
pnpm run docs --out ../wiki/api-docs/decky-frontend-lib
- name: Commit files
run: |

View File

@@ -19,11 +19,11 @@ jobs:
with:
node-version: 16
- name: Setup | Dependencies
run: npm ci
run: npm i -g pnpm && pnpm i --frozen-lockfile
- name: Test
run: npm test
run: pnpm run test
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm exec semantic-release
run: pnpm exec semantic-release

3
.gitignore vendored
View File

@@ -36,7 +36,4 @@ dist/
research/
# PNPM lockfile
pnpm-lock.yaml
docs/

View File

@@ -1,3 +1,60 @@
## [3.7.4](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.7.3...v3.7.4) (2022-10-26)
### Bug Fixes
* **docs:** change arg format ([ed0b92d](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/ed0b92de2ec13a585f6524b45eef0ab538d87448))
* **tabs:** fix on stable ([f16e0b2](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/f16e0b29f8e1de500e8f436db659d1ad99d4eaa6))
## [3.7.3](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.7.2...v3.7.3) (2022-10-25)
### Bug Fixes
* **tabs:** it returns ([3c553a2](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/3c553a227d1aa7b03c4431ff968f336b4f871801))
## [3.7.2](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.7.1...v3.7.2) (2022-10-24)
### Bug Fixes
* **tabs:** unkill build ([3dbca1a](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/3dbca1a0567592a597e70ce5e9bef157f709c765))
## [3.7.1](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.7.0...v3.7.1) (2022-10-24)
### Bug Fixes
* **Tabs:** temp remove until we have a way to grab it on beta ([25c33b2](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/25c33b2a05a30c3c72008c5f459c3b77f819db5a))
# [3.7.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.6.1...v3.7.0) (2022-10-24)
### Features
* **modal:** support for latest steamos preview ([5f0470c](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/5f0470c351dc4ecb24ea3e928ff0b0199c399fa4))
## [3.6.1](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.6.0...v3.6.1) (2022-10-19)
### Bug Fixes
* **plugin:** export RoutePatch ([#39](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/39)) ([c44c66f](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/c44c66facd4e158aa4fe0a69f62a2ca3add805c1))
# [3.6.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.5.6...v3.6.0) (2022-10-15)
### Features
* **plugin:** add alwaysRender ([2fc2060](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/2fc2060a6c0d9414d1c36a1a022fdc6f2cd7f8bb))
## [3.5.6](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.5.5...v3.5.6) (2022-10-08)
### Bug Fixes
* **Dialog:** remove not exported dialog button ([#37](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/37)) ([5a5218a](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/5a5218a7c43f6a90fc4de5f7a0cd524d1cd298d6))
## [3.5.5](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.5.4...v3.5.5) (2022-10-08)

22523
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "decky-frontend-lib",
"version": "3.5.5",
"version": "3.7.4",
"description": "A library for building decky plugins",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -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",

5284
pnpm-lock.yaml generated Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -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<ColorPickerModalProps> = ({
closeModal,
onConfirm = () => {},
title = "Color Picker",
title = 'Color Picker',
defaultH = 0,
defaultS = 100,
defaultL = 50,
@@ -26,22 +27,22 @@ export const ColorPickerModal: FC<ColorPickerModalProps> = ({
const [A, setA] = useState<number>(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 (
<ConfirmModal
bAllowFullSize
onCancel={closeModal}
onOK={() => {
onConfirm(`hsla(${H}, ${S}%, ${L}%, ${A})`);
closeModal();
}}
>
<style>
<ConfirmModal
bAllowFullSize
onCancel={closeModal}
onOK={() => {
onConfirm(`hsla(${H}, ${S}%, ${L}%, ${A})`);
closeModal();
}}
>
<style>
{`
/* This removes the cyan track color that is behind the slider head */
.ColorPicker_Container .${gamepadSliderClasses.SliderTrack} {
@@ -87,77 +88,44 @@ export const ColorPickerModal: FC<ColorPickerModalProps> = ({
}
`}
</style>
<div
className="ColorPicker_ColorDisplayContainer"
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '1em',
// theres a large header by default on the modal, so this just pushes it up into that unused space
marginTop: '-2.5em',
}}
>
<div>
<span style={{ fontSize: '1.5em' }}>
<b>{title}</b>
</span>
</div>
<div
className="ColorPicker_ColorDisplayContainer"
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "1em",
// theres a large header by default on the modal, so this just pushes it up into that unused space
marginTop: "-2.5em",
backgroundColor: `hsla(${H}, ${S}%, ${L}%, ${A})`,
width: '40px',
height: '40px',
}}
>
<div>
<span style={{ fontSize: "1.5em" }}>
<b>{title}</b>
</span>
</div>
<div
style={{
backgroundColor: `hsla(${H}, ${S}%, ${L}%, ${A})`,
width: "40px",
height: "40px",
}}
></div>
></div>
</div>
<div className="ColorPicker_Container" style={colorPickerCSSVars}>
<div className="ColorPicker_HSlider">
<SliderField showValue editableValue label="Hue" value={H} min={0} max={360} onChange={setH} />
</div>
<div className="ColorPicker_Container" style={colorPickerCSSVars}>
<div className="ColorPicker_HSlider">
<SliderField
showValue
editableValue
label="Hue"
value={H}
min={0}
max={360}
onChange={setH}
/>
</div>
<div className="ColorPicker_SSlider">
<SliderField
showValue
editableValue
label="Saturation"
value={S}
min={0}
max={100}
onChange={setS}
/>
</div>
<div className="ColorPicker_LSlider">
<SliderField
showValue
editableValue
label="Lightness"
value={L}
min={0}
max={100}
onChange={setL}
/>
</div>
<div className="ColorPicker_ASlider">
<SliderField
showValue
editableValue
label="Alpha"
value={A}
step={0.1}
min={0}
max={1}
onChange={setA}
/>
</div>
<div className="ColorPicker_SSlider">
<SliderField showValue editableValue label="Saturation" value={S} min={0} max={100} onChange={setS} />
</div>
</ConfirmModal>
<div className="ColorPicker_LSlider">
<SliderField showValue editableValue label="Lightness" value={L} min={0} max={100} onChange={setL} />
</div>
<div className="ColorPicker_ASlider">
<SliderField showValue editableValue label="Alpha" value={A} step={0.1} min={0} max={1} onChange={setA} />
</div>
</div>
</ConfirmModal>
);
};

View File

@@ -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<HTMLImageElement> {
suspenseWidth?: string | number;
suspenseHeight?: string | number;
@@ -38,4 +39,4 @@ export const SuspensefulImage: FC<SuspensefulImageProps> = (props) => {
) : (
<img {...props} />
);
};
};

View File

@@ -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);
};
}, []);

View File

@@ -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<ButtonProps>;

View File

@@ -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<ButtonItemProps>;

View File

@@ -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<HTMLDivElement> {
autoFocus?: boolean;
@@ -22,7 +23,6 @@ export interface CarouselProps extends HTMLAttributes<HTMLDivElement> {
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<CarouselProps & RefAttributes<HTMLDivElement>>;
}) as VFC<CarouselProps & RefAttributes<HTMLDivElement>>;

View File

@@ -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<HTMLDivElement> {
@@ -22,42 +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<DialogCommonProps>;
export const DialogSubHeader = MappedDialogDivs.get("DialogSubHeader") as FC<DialogCommonProps>;
export const DialogFooter = MappedDialogDivs.get("DialogFooter") as FC<DialogCommonProps>;
export const DialogLabel = MappedDialogDivs.get("DialogLabel") as FC<DialogCommonProps>;
export const DialogBodyText = MappedDialogDivs.get("DialogBodyText") as FC<DialogCommonProps>;
export const DialogBody = MappedDialogDivs.get("DialogBody") as FC<DialogCommonProps>;
export const DialogControlsSection = MappedDialogDivs.get("DialogControlsSection") as FC<DialogCommonProps>;
export const DialogControlsSectionHeader = MappedDialogDivs.get("DialogControlsSectionHeader") as FC<DialogCommonProps>;
export const DialogHeader = MappedDialogDivs.get('DialogHeader') as FC<DialogCommonProps>;
export const DialogSubHeader = MappedDialogDivs.get('DialogSubHeader') as FC<DialogCommonProps>;
export const DialogFooter = MappedDialogDivs.get('DialogFooter') as FC<DialogCommonProps>;
export const DialogLabel = MappedDialogDivs.get('DialogLabel') as FC<DialogCommonProps>;
export const DialogBodyText = MappedDialogDivs.get('DialogBodyText') as FC<DialogCommonProps>;
export const DialogBody = MappedDialogDivs.get('DialogBody') as FC<DialogCommonProps>;
export const DialogControlsSection = MappedDialogDivs.get('DialogControlsSection') as FC<DialogCommonProps>;
export const DialogControlsSectionHeader = MappedDialogDivs.get('DialogControlsSectionHeader') as FC<DialogCommonProps>;
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<DialogButtonProps>;
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<DialogButtonProps>;
export const DialogButtonSmall = Object.values(CommonUIModule).find(
(mod: any) =>
mod?.render?.toString()?.includes('Object.assign({type:"button"') &&
mod?.render?.toString()?.includes('DialogButton') &&
mod?.render?.toString()?.includes('Small')
) as FC<DialogButtonProps>;
// 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;

View File

@@ -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<HTMLDivElement>, 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<HTMLDivElement>, 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<FieldProps & RefAttributes<HTMLDivElement>>;

View File

@@ -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<FocusRingProps>;
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>;

View File

@@ -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<HTMLDivElement>, 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<FocusableProps & RefAttributes<HTMLDivElement>>;
}) as VFC<FocusableProps & RefAttributes<HTMLDivElement>>;

View File

@@ -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<GamepadEventDetail>
export type GamepadEvent = CustomEvent<GamepadEventDetail>;
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;
}
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;
}

View File

@@ -1,4 +1,6 @@
import { FC, ReactNode } from 'react';
import { findSP } from '../utils';
import { findModuleChild } from '../webpack';
// All of the popout options + strTitle are related. Proper usage is not yet known...
@@ -20,21 +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:
// <ModalRoot closeModal={() => { console.log("ABOUT TO CLOSE"); showModalRes.Close(); }} />
Update: (modal: ReactNode) => void;
}
export const showModal: (modal: ReactNode, parent?: EventTarget, props?: ShowModalProps) => Promise<ShowModalResult> = 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<ShowModalResult> =
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<ShowModalResult> => {
return showModalRaw(modal, parent || findSP(), props);
};
export interface ModalRootProps {
children?: ReactNode;

View File

@@ -1,4 +1,4 @@
import { VFC, ReactNode } from 'react';
import { ReactNode, VFC } from 'react';
import { findModuleChild } from '../webpack';
import { ItemProps } from './Item';

View File

@@ -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;

View File

@@ -11,7 +11,7 @@ export interface SidebarNavigationPage {
identifier?: string;
route?: string;
link?: string;
padding?: "none" | "compact";
padding?: 'none' | 'compact';
}
export interface SidebarNavigationProps {

View File

@@ -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<SVGAttributes<SVGElement>>;
export const Spinner = Object.values(IconsModule).find(
(mod: any) => mod?.toString && /Spinner\)}\),.\.createElement\(\"path\",{d:\"M18 /.test(mod.toString()),
) as FC<SVGAttributes<SVGElement>>;

View File

@@ -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<SVGAttributes<SVGElement>>;
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<SVGAttributes<SVGElement>>;

View File

@@ -1,10 +1,13 @@
import { FC, ReactNode } from 'react';
import { FC, ReactNode, createElement, useEffect, useState } from 'react';
import { fakeRenderComponent, findInReactTree, sleep } from '../utils';
import { findModule } from '../webpack';
import { FooterLegendProps } from './FooterLegend';
import { SteamSpinner } from './SteamSpinner';
/**
* Individual tab objects for the Tabs component
*
*
* `id` ID of this tab, can be used with activeTab to auto-focus a given tab
* `title` Title shown in the header bar
* `renderTabAddon` Return a {@link ReactNode} to render it next to the tab title, i.e. the counts for each tab on the Media page
@@ -12,22 +15,22 @@ import { FooterLegendProps } from './FooterLegend';
* `footer` Sets up button handlers and labels
*/
export interface Tab {
id: string;
title: string;
renderTabAddon?: () => ReactNode;
content: ReactNode;
footer?: FooterLegendProps;
id: string;
title: string;
renderTabAddon?: () => ReactNode;
content: ReactNode;
footer?: FooterLegendProps;
}
/**
* Props for the {@link Tabs}
*
*
* `tabs` array of {@link Tab}
* `activeTab` tab currently active, needs to be one of the tabs {@link Tab.id}, must be set using a `useState` in the `onShowTab` handler
* `onShowTab` Called when the active tab should change, needs to set `activeTab`. See example.
* `autoFocusContents` Whether to automatically focus the tab contents or not.
* `footer` Sets up button handlers and labels
*
*
* @example
* const Component: FC = () => {
* const [currentTab, setCurrentTab] = useState<string>("Tab1");
@@ -56,19 +59,77 @@ export interface Tab {
* };
*/
export interface TabsProps {
tabs: Tab[];
activeTab: string;
onShowTab: (tab: string) => void;
autoFocusContents?: boolean;
tabs: Tab[];
activeTab: string;
onShowTab: (tab: string) => void;
autoFocusContents?: boolean;
}
declare global {
interface Window {
DeckyPluginLoader: any;
}
}
let tabsComponent: any;
const getTabs = async () => {
if (tabsComponent) return tabsComponent;
while (!window?.DeckyPluginLoader?.routerHook?.routes) {
console.debug('[DFL:Tabs]: Waiting for Decky router...');
await sleep(500);
}
return (tabsComponent = fakeRenderComponent(
() => {
return findInReactTree(
findInReactTree(
window.DeckyPluginLoader.routerHook.routes
.find((x: any) => x.props.path == '/library/app/:appid/achievements')
.props.children.type(),
(x) => x?.props?.scrollTabsTop,
).type({ appid: 1 }),
(x) => x?.props?.tabs,
).type;
},
{
useRef: () => ({ current: { reaction: { track: () => {} } } }),
useContext: () => ({ match: { params: { appid: 1 } } }),
useMemo: () => ({ data: {} }),
},
));
};
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;
});
if (oldTabsModule) oldTabs = Object.values(oldTabsModule).find((x: any) => x?.type?.toString()?.includes("((function(") && x?.type?.toString()?.includes("[\"tabs\""));
} catch (e) {
console.error("Error finding oldTabs:", e)
}
/**
* Tabs component as used in the library and media tabs. See {@link TabsProps}
* Unlike other components in `decky-frontend-lib`, this requires Decky Loader to be running.
*/
export const Tabs = Object.values(findModule((m) => {
if (typeof m !== 'object') return false;
for (let prop in m) {
if (m[prop]?.Unbleed) return true;
}
return false;
})).find((x: any) => x?.type?.toString()?.includes("((function(){")) as FC<TabsProps>;
export const Tabs = oldTabs || ((props: TabsProps) => {
const found = tabsComponent;
const [tc, setTC] = useState<FC<TabsProps>>(found);
useEffect(() => {
if (found) return;
(async () => {
console.debug('[DFL:Tabs]: Finding component...');
const t = await getTabs();
console.debug('[DFL:Tabs]: Found!');
setTC(t);
})();
}, []);
console.log('tc', tc);
return tc ? createElement(tc, props) : <SteamSpinner />;
}) as FC<TabsProps>;

View File

@@ -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'));
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'),
);

View File

@@ -1 +1 @@
export * from './useParams'
export * from './useParams';

View File

@@ -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>() => T
export const useParams = Object.values(ReactRouter).find((val) => /return (\w)\?\1\.params:{}/.test(`${val}`)) as <
T,
>() => T;

View File

@@ -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';

View File

@@ -6,6 +6,7 @@ export interface Plugin {
icon: JSX.Element;
content?: JSX.Element;
onDismount?(): void;
alwaysRender?: boolean;
}
interface ServerResponseSuccess<TRes> {
@@ -20,7 +21,7 @@ interface ServerResponseError {
export type ServerResponse<TRes> = ServerResponseSuccess<TRes> | ServerResponseError;
type RoutePatch = (route: RouteProps) => RouteProps;
export type RoutePatch = (route: RouteProps) => RouteProps;
export interface RouterHook {
addRoute(path: string, component: ComponentType, props?: Omit<RouteProps, 'path' | 'children'>): void;
@@ -37,8 +38,8 @@ export interface ToastData {
icon?: ReactNode;
className?: string;
contentClassName?: string;
duration?: number
critical?: boolean
duration?: number;
critical?: boolean;
}
export interface Toaster {
@@ -53,7 +54,7 @@ export interface FilePickerRes {
export interface ServerAPI {
routerHook: RouterHook;
toaster: Toaster;
openFilePicker(startPath: string, includeFiles?: boolean, regex?: RegExp): Promise<FilePickerRes>
openFilePicker(startPath: string, includeFiles?: boolean, regex?: RegExp): Promise<FilePickerRes>;
callPluginMethod<TArgs = {}, TRes = {}>(methodName: string, args: TArgs): Promise<ServerResponse<TRes>>;
callServerMethod<TArgs = {}, TRes = {}>(methodName: string, args: TArgs): Promise<ServerResponse<TRes>>;
fetchNoCors<TRes = {}>(url: RequestInfo, request?: RequestInit): Promise<ServerResponse<TRes>>;

View File

@@ -1,10 +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;
}

View File

@@ -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<Patch>();
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})
}
patch.hasUnpatched = true;
console.debug('[Patcher] unpatched', {
realObject,
realProp,
object,
property,
handler,
patchedFunction,
original,
isEqual: realObject[realProp] === patchedFunction,
});
}

View File

@@ -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' ]
});
export const findInReactTree = (node: any, filter: findInTreeFilter) =>
findInTree(node, filter, {
// Specialised findInTree for React nodes
walkable: ['props', 'children', 'child', 'sibling'],
});

View File

@@ -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) {