Compare commits

...

14 Commits

Author SHA1 Message Date
Jonas Dellinger
f101e23c1c 0.0.6 2022-06-02 17:58:18 +02:00
Jonas Dellinger
5b3ea8f63f 0.0.5 2022-06-02 17:57:48 +02:00
AAGaming
07ccceb990 add modal support, showModal->showContextMenu and new showModal 2022-06-01 17:51:17 -04:00
Jonas Dellinger
536269b1b7 Expose routerhook 2022-05-30 21:00:38 +02:00
Jonas Dellinger
1e92d5c754 0.0.4 2022-05-30 20:51:27 +02:00
AAGaming
668ab618fc add patcher and react utils 2022-05-29 20:19:10 -04:00
AAGaming
5d59bf348c fix react 2022-05-28 21:49:38 -04:00
AAGaming
a5ac702820 fix typescript AGAIN, fix icons, ignore pnpm lock 2022-05-28 20:54:25 -04:00
Jonas Dellinger
f882636be7 0.0.3 2022-05-26 13:29:00 +02:00
Jonas Dellinger
815129d03f Export DialogButton and add icon prop 2022-05-26 13:28:35 +02:00
Jonas Dellinger
ef41d17e43 Add back panels 2022-05-26 10:15:32 +02:00
AAGaming
6d4ca5dc92 Merge branch 'main' of github.com:steamdeckhomebrew/decky-frontend-lib 2022-05-25 20:26:43 -04:00
AAGaming
0303724794 export more stuff, replace button component with actual button and move the old one to ButtonItem 2022-05-25 20:26:30 -04:00
Jonas Dellinger
98c226b8e6 Remove pnpm lock 2022-05-25 21:03:03 +02:00
18 changed files with 275 additions and 3017 deletions

3
.gitignore vendored
View File

@@ -35,3 +35,6 @@ Thumbs.db
dist/
research/
# PNPM lockfile
pnpm-lock.yaml

5
globals.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare global {
interface Window {
SP_REACT: typeof React;
}
}

136
package-lock.json generated
View File

@@ -1,26 +1,23 @@
{
"name": "decky-frontend-lib",
"version": "0.0.1",
"version": "0.0.6",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "decky-frontend-lib",
"version": "0.0.1",
"version": "0.0.6",
"license": "GPL-2.0-or-later",
"devDependencies": {
"@types/jest": "^27.4.1",
"@types/react": "16.14.0",
"@types/react-router": "5.1.18",
"import-sort-style-module": "^6.0.0",
"jest": "^27.5.1",
"prettier-plugin-import-sort": "^0.0.7",
"react": "^16.14.0",
"shx": "^0.3.4",
"ts-jest": "^27.1.4",
"typescript": "^4.6.3"
},
"peerDependencies": {
"react": "16.14.0"
}
},
"node_modules/@ampproject/remapping": {
@@ -1255,6 +1252,12 @@
"@types/node": "*"
}
},
"node_modules/@types/history": {
"version": "4.7.11",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
"integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==",
"dev": true
},
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
@@ -1317,6 +1320,16 @@
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-router": {
"version": "5.1.18",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.18.tgz",
"integrity": "sha512-YYknwy0D0iOwKQgz9v8nOzt2J6l4gouBmDnWqUUznltOTaon+r8US8ky8HvN0tXvc38U9m6z/t2RsVsnd1zM0g==",
"dev": true,
"dependencies": {
"@types/history": "^4.7.11",
"@types/react": "*"
}
},
"node_modules/@types/stack-utils": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
@@ -4695,18 +4708,6 @@
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
"dev": true
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -4867,15 +4868,6 @@
"integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==",
"dev": true
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -5129,17 +5121,6 @@
"node": ">= 6"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/psl": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
@@ -5155,26 +5136,6 @@
"node": ">=6"
}
},
"node_modules/react": {
"version": "16.14.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
"integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==",
"dev": true,
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
},
"node_modules/rechoir": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
@@ -6978,6 +6939,12 @@
"@types/node": "*"
}
},
"@types/history": {
"version": "4.7.11",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
"integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==",
"dev": true
},
"@types/istanbul-lib-coverage": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
@@ -7040,6 +7007,16 @@
"csstype": "^3.0.2"
}
},
"@types/react-router": {
"version": "5.1.18",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.18.tgz",
"integrity": "sha512-YYknwy0D0iOwKQgz9v8nOzt2J6l4gouBmDnWqUUznltOTaon+r8US8ky8HvN0tXvc38U9m6z/t2RsVsnd1zM0g==",
"dev": true,
"requires": {
"@types/history": "^4.7.11",
"@types/react": "*"
}
},
"@types/stack-utils": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
@@ -9599,15 +9576,6 @@
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
"dev": true
},
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -9738,12 +9706,6 @@
"integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==",
"dev": true
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"dev": true
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -9927,17 +9889,6 @@
"sisteransi": "^1.0.5"
}
},
"prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"psl": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
@@ -9950,23 +9901,6 @@
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
"dev": true
},
"react": {
"version": "16.14.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
"integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==",
"dev": true,
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2"
}
},
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
},
"rechoir": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "decky-frontend-lib",
"version": "0.0.2",
"version": "0.0.6",
"description": "A library for building decky plugins",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -35,6 +35,7 @@
"devDependencies": {
"@types/jest": "^27.4.1",
"@types/react": "16.14.0",
"@types/react-router": "5.1.18",
"import-sort-style-module": "^6.0.0",
"jest": "^27.5.1",
"prettier-plugin-import-sort": "^0.0.7",

2899
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,14 +3,27 @@ import { FC } from 'react';
import { CommonUIModule } from '../webpack';
interface ButtonProps {
label?: string;
description?: string;
layout?: 'below';
onClick?(e: MouseEvent): void;
className?: string;
noFocusRing?: boolean;
disabled?: boolean;
bottomSeparator?: boolean;
onClick?(e: MouseEvent): void;
onPointerDown?(e: PointerEvent): void;
onPointerUp?(e: PointerEvent): void;
onPointerCancel?(e: PointerEvent): void;
onMouseDown?(e: PointerEvent): void;
onMouseUp?(e: MouseEvent): void;
onTouchStart?(e: TouchEvent): void;
onTouchEnd?(e: TouchEvent): void;
onTouchCancel?(e: TouchEvent): void;
onSubmit?(e: SubmitEvent): void;
}
export const Button = Object.values(CommonUIModule).find((mod: any) =>
mod?.render?.toString()?.includes('childrenContainerWidth:"min"'),
) as FC<ButtonProps>;
export const DialogButton = Object.values(CommonUIModule).find(
(mod: any) =>
mod?.render?.toString()?.includes('Object.assign({type:"button"') &&
mod?.render?.toString()?.includes('DialogButton'),
) as any;
// Button isn't exported, so call DialogButton to grab it
export const Button = DialogButton!.render({}).type as FC<ButtonProps>; // its actually a forwarded ref but that doesn't really matter in usage

View File

@@ -0,0 +1,17 @@
import { FC } from 'react';
import { CommonUIModule } from '../webpack';
interface ButtonItemProps {
label?: string;
description?: string;
layout?: 'below';
icon?: JSX.Element;
onClick?(e: MouseEvent): void;
disabled?: boolean;
bottomSeparator?: boolean;
}
export const ButtonItem = Object.values(CommonUIModule).find((mod: any) =>
mod?.render?.toString()?.includes('childrenContainerWidth:"min"'),
) as FC<ButtonItemProps>;

11
src/deck-components/Menu.tsx Normal file → Executable file
View File

@@ -1,7 +1,16 @@
import { FC } from 'react';
import { FC, ReactNode } from 'react';
import { findModuleChild } from '../webpack';
export const showContextMenu: (children: ReactNode, parent?: EventTarget) => void = findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (typeof m[prop] === 'function' && m[prop].toString().includes('stopPropagation))')) {
return m[prop];
}
}
});
interface MenuProps {
label: string;
onCancel?(): void;

24
src/deck-components/Modal.tsx Normal file → Executable file
View File

@@ -1,12 +1,28 @@
import { ReactNode } from 'react';
import { FC, ReactNode } from 'react';
import { findModuleChild } from '../webpack';
export const showModal: (children: ReactNode, parent: EventTarget) => void = findModuleChild((m) => {
// TODO: there is another argument, figure out what it does
export const showModal: (children: ReactNode, parent?: EventTarget) => void = findModuleChild((m) => {
if (typeof m !== 'object') return undefined;
for (let prop in m) {
if (typeof m[prop] === 'function' && m[prop].toString().includes('stopPropagation))')) {
if (typeof m[prop] === 'function' && m[prop].toString().includes('bHideMainWindowForPopouts:!0')) {
return m[prop];
}
}
});
interface ModalRootProps {
onMiddleButton?(): void,
onCancel?(): void;
onOK?(): void;
bAllowFullSize?: boolean;
}
export const ModalRoot = findModuleChild(m => {
if (typeof m !== "object") return undefined;
for (let prop in m) {
if (!m[prop]?.prototype?.OK && m[prop]?.prototype?.Cancel && m[prop]?.prototype?.render) {
return m[prop];
}
}
}) as FC<ModalRootProps>;

View File

@@ -0,0 +1,23 @@
import { FC } from 'react';
import { findModuleChild } from '../webpack';
interface PanelSectionProps {
title?: string;
spinner?: boolean;
}
const [panelSection, mod] = findModuleChild((mod: any) => {
for (let prop in mod) {
if (mod[prop]?.toString()?.includes('.PanelSection')) {
return [mod[prop], mod];
}
}
return null;
});
export const PanelSection = panelSection as FC<PanelSectionProps>;
export const PanelSectionRow = Object.values(mod).filter(
(exp: any) => !exp?.toString()?.includes('.PanelSection'),
)[0] as FC;

View File

@@ -0,0 +1,8 @@
import { FC } 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<{}>;

View File

@@ -0,0 +1,9 @@
import { FC } 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<{}>;

4
src/deck-components/index.ts Normal file → Executable file
View File

@@ -1,6 +1,10 @@
export * from './Button';
export * from './ButtonItem';
export * from './Menu';
export * from './Modal';
export * from './Panel';
export * from './Slider';
export * from './SteamSpinner';
export * from './Spinner';
export * from './static-classes';
export * from './Toggle';

View File

@@ -2,3 +2,4 @@
export * from './deck-components';
export * from './plugin';
export * from './webpack';
export * from './utils';

View File

@@ -1,3 +1,6 @@
import type { ComponentType } from 'react';
import { RouteProps } from 'react-router';
export interface Plugin {
title: JSX.Element;
icon: JSX.Element;
@@ -17,7 +20,13 @@ interface ServerResponseError {
type ServerResponse<TRes> = ServerResponseSuccess<TRes> | ServerResponseError;
interface RouterHook {
addRoute(path: string, component: ComponentType, props?: Omit<RouteProps, 'path' | 'children'>): void;
removeRoute(path: string): void;
}
export interface ServerAPI {
routerHook: RouterHook;
callPluginMethod<TArgs = {}, TRes = {}>(methodName: string, args: TArgs): Promise<ServerResponse<TRes>>;
callServerMethod<TArgs = {}, TRes = {}>(methodName: string, args: TArgs): Promise<ServerResponse<TRes>>;
fetchNoCors<TRes = {}>(url: string, request: RequestInfo): Promise<ServerResponse<TRes>>;

87
src/utils.ts Normal file
View File

@@ -0,0 +1,87 @@
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;
}
}
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;
// 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,
}
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];
};
const res = fun(hooks);
Object.assign(hooks, oldHooks);
return res;
}
export function beforePatch(obj: any, name: string, fnc: Function): void {
const orig = obj[name];
obj[name] = function (...args: any[]) {
fnc.call(this, args);
return orig.call(this, ...args);
}
Object.assign(obj[name], orig);
obj[name].toString = () => orig.toString();
obj[name].__deckyOrig = orig;
}
export function afterPatch(obj: any, name: string, fnc: Function): void {
const orig = obj[name];
obj[name] = function (...args: any[]) {
let ret = orig.call(this, ...args);
ret = fnc.call(this, args, ret);
return ret;
}
Object.assign(obj[name], orig);
obj[name].toString = () => orig.toString();
obj[name].__deckyOrig = orig;
}
export function replacePatch(obj: any, name: string, fnc: Function): void {
const orig = obj[name];
obj[name] = function (...args: any[]) {
const ret = fnc.call(this, args);
if (ret == 'CALL_ORIGINAL') return orig.call(this, ...args);
return ret;
};
Object.assign(obj[name], orig);
obj[name].toString = () => orig.toString();
obj[name].__deckyOrig = orig;
}
// TODO allow one method to be patched and unpatched multiple times independently using IDs in a Map or something
export function unpatch(obj: any, name: any): void {
obj[name] = obj[name].__deckyOrig;
}
export function getReactInstance(o: HTMLElement | Element | Node) {
return o[Object.keys(o).find(k => k.startsWith('__reactInternalInstance')) as string]
}

View File

@@ -58,7 +58,24 @@ export const CommonUIModule = allModules.find((m: Module) => {
return false;
});
export const IconsModule = findModule((m: Module) => {
if (typeof m !== "object") return false;
for (let prop in m) {
if (m[prop]?.toString && /Spinner\)}\),.\.createElement\(\"path\",{d:\"M18 /.test(m[prop].toString())) return true;
}
return false;
})
export const Router = findModuleChild((m: Module) => {
if (typeof m !== "object") return undefined;
for (let prop in m) { if (m[prop]?.Navigate && m[prop]?.NavigationManager) return m[prop]}
for (let prop in m) {
if (m[prop]?.Navigate && m[prop]?.NavigationManager) return m[prop]
}
})
export const ReactRouter = allModules.find((m: Module) => {
if (typeof m !== "object") return undefined;
for (let prop in m) {
if (m[prop]?.computeRootMatch) return true
}
return false;
})

View File

@@ -3,7 +3,8 @@
"outDir": "dist",
"module": "ESNext",
"target": "ES2020",
"jsx": "react-jsx",
"jsx": "react",
"jsxFactory": "window.SP_REACT.createElement",
"declaration": true,
"moduleResolution": "node",
"noUnusedLocals": true,
@@ -17,6 +18,6 @@
"allowSyntheticDefaultImports": true,
"skipLibCheck": true
},
"include": ["src"],
"include": ["src", "globals.d.ts"],
"exclude": ["node_modules"]
}