mirror of
https://github.com/SteamDeckHomebrew/decky-frontend-lib.git
synced 2026-05-26 21:06:57 +02:00
Merge branch 'SteamDeckHomebrew:main' into main
This commit is contained in:
1
src/custom-hooks/index.ts
Normal file
1
src/custom-hooks/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './usequickaccessvisible';
|
||||
67
src/custom-hooks/usequickaccessvisible.tsx
Normal file
67
src/custom-hooks/usequickaccessvisible.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
declare global {
|
||||
var FocusNavController: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns state indicating the visibility of quick access menu.
|
||||
*
|
||||
* @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";
|
||||
*
|
||||
* export const PluginPanelView: VFC<{}> = ({ }) => {
|
||||
* const isVisible = useQuickAccessVisible();
|
||||
*
|
||||
* useEffect(() => {
|
||||
* if (!isVisible) {
|
||||
* return;
|
||||
* }
|
||||
*
|
||||
* const interval = setInterval(() => console.log("Hello world!"), 1000);
|
||||
* return () => {
|
||||
* clearInterval(interval);
|
||||
* }
|
||||
* }, [isVisible])
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {isVisible ? "VISIBLE" : "INVISIBLE"}
|
||||
* </div>
|
||||
* );
|
||||
* };
|
||||
*/
|
||||
export function useQuickAccessVisible(): boolean {
|
||||
// Assuming that the component is rendered in QAM already, so true by default...
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
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!");
|
||||
return;
|
||||
}
|
||||
|
||||
const onBlur = () => setIsVisible(false);
|
||||
const onFocus = () => setIsVisible(true);
|
||||
|
||||
quickAccessWindow.addEventListener("blur", onBlur);
|
||||
quickAccessWindow.addEventListener("focus", onFocus);
|
||||
return () => {
|
||||
quickAccessWindow.removeEventListener("blur", onBlur);
|
||||
quickAccessWindow.removeEventListener("focus", onFocus);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return isVisible;
|
||||
}
|
||||
@@ -1,22 +1,8 @@
|
||||
import { CSSProperties, FC, RefAttributes } from 'react';
|
||||
import { FC } from 'react';
|
||||
import { DialogButton, DialogButtonProps } from "./Dialog";
|
||||
|
||||
import { CommonUIModule } from '../webpack';
|
||||
import { FooterLegendProps } from './FooterLegend';
|
||||
|
||||
export interface DialogButtonProps extends RefAttributes<HTMLDivElement>, FooterLegendProps {
|
||||
label?: string;
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
noFocusRing?: boolean;
|
||||
description?: string;
|
||||
layout?: 'below';
|
||||
onClick?(e: MouseEvent): void;
|
||||
disabled?: boolean;
|
||||
bottomSeparator?: boolean;
|
||||
export interface ButtonProps extends DialogButtonProps {
|
||||
}
|
||||
|
||||
export const DialogButton = Object.values(CommonUIModule).find(
|
||||
(mod: any) =>
|
||||
mod?.render?.toString()?.includes('Object.assign({type:"button"') &&
|
||||
mod?.render?.toString()?.includes('DialogButton'),
|
||||
) as FC<DialogButtonProps>;
|
||||
// Button isn't exported, so call DialogButton to grab it
|
||||
export const Button = (DialogButton as any)?.render({}).type as FC<ButtonProps>;
|
||||
|
||||
56
src/deck-components/Dialog.tsx
Normal file
56
src/deck-components/Dialog.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { CommonUIModule } from "../webpack";
|
||||
import { CSSProperties, FC, RefAttributes } from "react";
|
||||
import { FooterLegendProps } from './FooterLegend';
|
||||
|
||||
export interface DialogCommonProps extends RefAttributes<HTMLDivElement> {
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface DialogButtonProps extends DialogCommonProps, FooterLegendProps {
|
||||
noFocusRing?: boolean;
|
||||
disabled?: boolean;
|
||||
onClick?(e: MouseEvent): void;
|
||||
onPointerDown?(e: PointerEvent): void;
|
||||
onPointerUp?(e: PointerEvent): void;
|
||||
onPointerCancel?(e: PointerEvent): void;
|
||||
onMouseDown?(e: MouseEvent): void;
|
||||
onMouseUp?(e: MouseEvent): void;
|
||||
onTouchStart?(e: TouchEvent): void;
|
||||
onTouchEnd?(e: TouchEvent): void;
|
||||
onTouchCancel?(e: TouchEvent): void;
|
||||
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]
|
||||
}));
|
||||
|
||||
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')
|
||||
) 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')
|
||||
) as FC<DialogButtonProps>;
|
||||
|
||||
// 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;
|
||||
@@ -15,6 +15,7 @@ export interface MenuProps {
|
||||
label: string;
|
||||
onCancel?(): void;
|
||||
cancelText?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const Menu: FC<MenuProps> = findModuleChild((m) => {
|
||||
@@ -27,8 +28,26 @@ export const Menu: FC<MenuProps> = findModuleChild((m) => {
|
||||
}
|
||||
});
|
||||
|
||||
export interface MenuGroupProps {
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const MenuGroup: FC<MenuGroupProps> = findModuleChild((m) => {
|
||||
if (typeof m !== 'object') return undefined;
|
||||
|
||||
for (let prop in m) {
|
||||
if (m[prop]?.prototype?.RenderSubMenu && m[prop]?.prototype?.ShowSubMenu) {
|
||||
return m[prop];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export interface MenuItemProps {
|
||||
onSelected?(): void;
|
||||
disabled?: boolean;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const MenuItem: FC<MenuItemProps> = findModuleChild((m) => {
|
||||
|
||||
@@ -1,9 +1,33 @@
|
||||
import { FC, ReactNode } from 'react';
|
||||
|
||||
import { findModuleChild } from '../webpack';
|
||||
|
||||
// TODO: there is another argument, figure out what it does
|
||||
export const showModal: (children: ReactNode, parent?: EventTarget) => void = findModuleChild((m) => {
|
||||
// All of the popout options + strTitle are related. Proper usage is not yet known...
|
||||
export interface ShowModalProps {
|
||||
browserContext?: unknown; // This is another Deck Object that is yet to be found
|
||||
bForcePopOut?: boolean;
|
||||
bHideActionIcons?: boolean;
|
||||
bHideMainWindowForPopouts?: boolean;
|
||||
bNeverPopOut?: boolean;
|
||||
fnOnClose?: () => void; // Seems to be the same as "closeModal" callback, but only when the modal is a popout. Will no longer work after "Update" invocation!
|
||||
popupHeight?: number;
|
||||
popupWidth?: number;
|
||||
promiseRenderComplete?: Promise<void>; // Invoked once the render is complete. Currently, it seems to be used as image loading success/error callback...
|
||||
strTitle?: string;
|
||||
}
|
||||
|
||||
export interface ShowModalResult {
|
||||
// This method will not invoke any of the variations of "closeModal" callbacks!
|
||||
Close: () => void;
|
||||
|
||||
// This method will replace the modal element completely and will not update the callback chains,
|
||||
// meaning that "closeModal" and etc. will not automatically close the modal anymore (also "fnOnClose"
|
||||
// will not be even called upon close anymore)! You have to manually call the "Close" method when, for example,
|
||||
// the "closeModal" is invoked in the newly updated modal:
|
||||
// <ModalRoot closeModal={() => { console.log("ABOUT TO CLOSE"); showModalRes.Close(); }} />
|
||||
Update: (modal: ReactNode) => void;
|
||||
}
|
||||
|
||||
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')) {
|
||||
@@ -13,6 +37,7 @@ export const showModal: (children: ReactNode, parent?: EventTarget) => void = fi
|
||||
});
|
||||
|
||||
export interface ModalRootProps {
|
||||
children?: ReactNode;
|
||||
onCancel?(): void;
|
||||
closeModal?(): void;
|
||||
onOK?(): void;
|
||||
@@ -24,10 +49,18 @@ export interface ModalRootProps {
|
||||
bDisableBackgroundDismiss?: boolean;
|
||||
bHideCloseIcon?: boolean;
|
||||
bOKDisabled?: boolean;
|
||||
bCancelDisabled?: boolean;
|
||||
}
|
||||
|
||||
export interface ConfirmModalProps extends ModalRootProps {
|
||||
onMiddleButton?(): void;
|
||||
onMiddleButton?(): void; // setting this prop will enable the middle button
|
||||
strTitle?: ReactNode;
|
||||
strDescription?: ReactNode;
|
||||
strOKButtonText?: ReactNode;
|
||||
strCancelButtonText?: ReactNode;
|
||||
strMiddleButtonText?: ReactNode;
|
||||
bAlertDialog?: boolean; // This will open a modal with only OK button enabled
|
||||
bMiddleDisabled?: boolean;
|
||||
}
|
||||
|
||||
export const ConfirmModal = findModuleChild((m) => {
|
||||
@@ -46,4 +79,4 @@ export const ModalRoot = findModuleChild((m) => {
|
||||
return m[prop];
|
||||
}
|
||||
}
|
||||
}) as FC<ModalRootProps>;
|
||||
}) as FC<ModalRootProps>;
|
||||
|
||||
@@ -2,17 +2,25 @@ import { ReactNode, VFC } from 'react';
|
||||
|
||||
import { Module, findModuleChild } from '../webpack';
|
||||
|
||||
export interface SidebarNavigationPages {
|
||||
export interface SidebarNavigationPage {
|
||||
title: string;
|
||||
route: string;
|
||||
content: ReactNode;
|
||||
icon?: ReactNode;
|
||||
visible?: boolean;
|
||||
hideTitle?: boolean;
|
||||
identifier?: string;
|
||||
route?: string;
|
||||
link?: string;
|
||||
padding?: "none" | "compact";
|
||||
}
|
||||
|
||||
export interface SidebarNavigationProps {
|
||||
title?: string;
|
||||
pages: SidebarNavigationPages[];
|
||||
pages: SidebarNavigationPage[];
|
||||
showTitle?: boolean;
|
||||
disableRouteReporting?: boolean;
|
||||
page?: string;
|
||||
onPageRequested?: (page: string) => void;
|
||||
}
|
||||
|
||||
export const SidebarNavigation = findModuleChild((mod: Module) => {
|
||||
|
||||
74
src/deck-components/Tabs.tsx
Normal file
74
src/deck-components/Tabs.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { FC, ReactNode } from 'react';
|
||||
import { findModule } from '../webpack';
|
||||
import { FooterLegendProps } from './FooterLegend';
|
||||
|
||||
/**
|
||||
* 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
|
||||
* `content` Content of the tab
|
||||
* `footer` Sets up button handlers and labels
|
||||
*/
|
||||
export interface Tab {
|
||||
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");
|
||||
*
|
||||
* return (
|
||||
* <Tabs
|
||||
* title="Theme Manager"
|
||||
* activeTab={currentTabRoute}
|
||||
* onShowTab={(tabID: string) => {
|
||||
* setCurrentTabRoute(tabID);
|
||||
* }}
|
||||
* tabs={[
|
||||
* {
|
||||
* title: "Tab 1",
|
||||
* content: <Tab1Component />,
|
||||
* id: "Tab1",
|
||||
* },
|
||||
* {
|
||||
* title: "Tab 2",
|
||||
* content: <Tab2Component />,
|
||||
* id: "Tab2",
|
||||
* },
|
||||
* ]}
|
||||
* />
|
||||
* );
|
||||
* };
|
||||
*/
|
||||
export interface TabsProps {
|
||||
tabs: Tab[];
|
||||
activeTab: string;
|
||||
onShowTab: (tab: string) => void;
|
||||
autoFocusContents?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tabs component as used in the library and media tabs. See {@link TabsProps}
|
||||
*/
|
||||
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>;
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from './Button';
|
||||
export * from './ButtonItem';
|
||||
export * from './Carousel';
|
||||
export * from './Dialog';
|
||||
export * from './Dropdown';
|
||||
export * from './Field';
|
||||
export * from './Focusable';
|
||||
@@ -16,6 +17,7 @@ export * from './SliderField';
|
||||
export * from './Spinner';
|
||||
export * from './static-classes';
|
||||
export * from './SteamSpinner';
|
||||
export * from './Tabs';
|
||||
export * from './TextField';
|
||||
export * from './Toggle';
|
||||
export * from './ToggleField';
|
||||
|
||||
1
src/deck-hooks/index.ts
Normal file
1
src/deck-hooks/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './useParams'
|
||||
15
src/deck-hooks/useParams.ts
Normal file
15
src/deck-hooks/useParams.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
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
|
||||
@@ -1,6 +1,8 @@
|
||||
// export * from './deck-libs';
|
||||
export * from './custom-components';
|
||||
export * from './custom-hooks';
|
||||
export * from './deck-components';
|
||||
export * from './deck-hooks'
|
||||
export * from './plugin';
|
||||
export * from './webpack';
|
||||
export * from './utils';
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface Plugin {
|
||||
icon: JSX.Element;
|
||||
content?: JSX.Element;
|
||||
onDismount?(): void;
|
||||
alwaysRender?: boolean;
|
||||
}
|
||||
|
||||
interface ServerResponseSuccess<TRes> {
|
||||
@@ -37,8 +38,8 @@ export interface ToastData {
|
||||
icon?: ReactNode;
|
||||
className?: string;
|
||||
contentClassName?: string;
|
||||
duration?: number
|
||||
critical?: boolean
|
||||
duration?: number;
|
||||
critical?: boolean;
|
||||
}
|
||||
|
||||
export interface Toaster {
|
||||
|
||||
Reference in New Issue
Block a user