Compare commits

..

4 Commits

Author SHA1 Message Date
semantic-release-bot
86f33de2c0 chore(release): 3.19.1 [CI SKIP] 2023-02-23 02:57:40 +00:00
AAGaming
0b6dc24c0d fix(*): refactoring to fix for feb 22 2023 beta 2023-02-22 21:56:46 -05:00
semantic-release-bot
18ce1ad790 chore(release): 3.19.0 [CI SKIP] 2023-02-22 03:38:02 +00:00
Travis Lane
5a074b5bb6 feat: added reorderable list and updated fieldProps (#57) 2023-02-22 03:37:26 +00:00
10 changed files with 286 additions and 88 deletions

View File

@@ -1,3 +1,17 @@
## [3.19.1](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.19.0...v3.19.1) (2023-02-23)
### Bug Fixes
* refactoring to fix for feb 22 2023 beta ([0b6dc24](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/0b6dc24c0da2d7644e185425e975787657f8bba1))
# [3.19.0](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.18.11...v3.19.0) (2023-02-22)
### Features
* added reorderable list and updated fieldProps ([#57](https://github.com/SteamDeckHomebrew/decky-frontend-lib/issues/57)) ([5a074b5](https://github.com/SteamDeckHomebrew/decky-frontend-lib/commit/5a074b5bb68c675c484a7b693f67a67488be9bcf))
## [3.18.11](https://github.com/SteamDeckHomebrew/decky-frontend-lib/compare/v3.18.10...v3.18.11) (2023-02-18)

View File

@@ -1,6 +1,6 @@
{
"name": "decky-frontend-lib",
"version": "3.18.11",
"version": "3.19.1",
"description": "A library for building decky plugins",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -0,0 +1,174 @@
import { Fragment, JSXElementConstructor, ReactElement, useEffect, useState } from 'react';
import { Field, FieldProps, Focusable, GamepadButton } from '../deck-components';
/**
* A ReorderableList entry of type <T>.
* @param label The name of this entry in the list.
* @param data Optional data to connect to this entry.
* @param position The position of this entry in the list.
*/
export type ReorderableEntry<T> = {
label: string;
data?: T;
position: number;
};
/**
* Properties for a ReorderableList component of type <T>.
*
* @param animate If the list should animate. @default true
*/
export type ReorderableListProps<T> = {
entries: ReorderableEntry<T>[];
onSave: (entries: ReorderableEntry<T>[]) => void;
interactables?: JSXElementConstructor<{ entry: ReorderableEntry<T> }>;
fieldProps?: FieldProps;
animate?: boolean;
};
/**
* A component for creating reorderable lists.
*
* See an example implementation {@linkplain https://github.com/Tormak9970/Component-Testing-Plugin/blob/main/src/testing-window/ReorderableListTest.tsx here}.
*/
export function ReorderableList<T>(props: ReorderableListProps<T>) {
if (props.animate === undefined) props.animate = true;
const [entryList, setEntryList] = useState<ReorderableEntry<T>[]>(
props.entries.sort((a: ReorderableEntry<T>, b: ReorderableEntry<T>) => a.position - b.position),
);
const [reorderEnabled, setReorderEnabled] = useState<boolean>(false);
useEffect(() => {
setEntryList(props.entries.sort((a: ReorderableEntry<T>, b: ReorderableEntry<T>) => a.position - b.position));
}, [props.entries]);
function toggleReorderEnabled(): void {
let newReorderValue = !reorderEnabled;
setReorderEnabled(newReorderValue);
if (!newReorderValue) {
props.onSave(entryList);
}
}
return (
<Fragment>
<div
style={{
width: 'inherit',
height: 'inherit',
flex: '1 1 1px',
scrollPadding: '48px 0px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-start',
alignContent: 'stretch',
}}
>
<Focusable
onSecondaryButton={toggleReorderEnabled}
onSecondaryActionDescription={reorderEnabled ? 'Save Order' : 'Reorder'}
onClick={toggleReorderEnabled}
>
{entryList.map((entry: ReorderableEntry<T>) => (
<ReorderableItem
animate={props.animate!}
listData={entryList}
entryData={entry}
reorderEntryFunc={setEntryList}
reorderEnabled={reorderEnabled}
fieldProps={props.fieldProps}
>
{props.interactables ? <props.interactables entry={entry} /> : null}
</ReorderableItem>
))}
</Focusable>
</div>
</Fragment>
);
}
/**
* Properties for a ReorderableItem component of type <T>
*/
export type ReorderableListEntryProps<T> = {
fieldProps?: FieldProps;
listData: ReorderableEntry<T>[];
entryData: ReorderableEntry<T>;
reorderEntryFunc: CallableFunction;
reorderEnabled: boolean;
animate: boolean;
children: ReactElement | null;
};
function ReorderableItem<T>(props: ReorderableListEntryProps<T>) {
const [isSelected, _setIsSelected] = useState<boolean>(false);
const [isSelectedLastFrame, setIsSelectedLastFrame] = useState<boolean>(false);
const listEntries = props.listData;
function onReorder(e: Event): void {
if (!props.reorderEnabled) return;
const event = e as CustomEvent;
const currentIdx = listEntries.findIndex((entryData: ReorderableEntry<T>) => entryData === props.entryData);
const currentIdxValue = listEntries[currentIdx];
if (currentIdx < 0) return;
let targetPosition: number = -1;
if (event.detail.button == GamepadButton.DIR_DOWN) {
targetPosition = currentIdxValue.position + 1;
} else if (event.detail.button == GamepadButton.DIR_UP) {
targetPosition = currentIdxValue.position - 1;
}
if (targetPosition >= listEntries.length || targetPosition < 0) return;
let otherToUpdate = listEntries.find((entryData: ReorderableEntry<T>) => entryData.position === targetPosition);
if (!otherToUpdate) return;
let currentPosition = currentIdxValue.position;
currentIdxValue.position = otherToUpdate.position;
otherToUpdate.position = currentPosition;
props.reorderEntryFunc(
[...listEntries].sort((a: ReorderableEntry<T>, b: ReorderableEntry<T>) => a.position - b.position),
);
}
async function setIsSelected(val: boolean) {
_setIsSelected(val);
// Wait 3 frames, then set. I have no idea why, but if you dont wait long enough it doesn't work.
for (let i = 0; i < 3; i++) await new Promise((res) => requestAnimationFrame(res));
setIsSelectedLastFrame(val);
}
return (
<div
style={
props.animate
? {
transition:
isSelected || isSelectedLastFrame
? ''
: 'transform 0.3s cubic-bezier(0.25, 1, 0.5, 1), opacity 0.3s cubic-bezier(0.25, 1, 0.5, 1)', // easeOutQuart https://easings.net/#easeOutQuart
transform: !props.reorderEnabled || isSelected ? 'scale(1)' : 'scale(0.9)',
opacity: !props.reorderEnabled || isSelected ? 1 : 0.7,
}
: {}
}
>
<Field
label={props.entryData.label}
{...props.fieldProps}
focusable={!props.children}
onButtonDown={onReorder}
onGamepadBlur={() => setIsSelected(false)}
onGamepadFocus={() => setIsSelected(true)}
>
<Focusable style={{ display: 'flex', width: '100%', position: 'relative' }}>{props.children}</Focusable>
</Field>
</div>
);
}

View File

@@ -1,2 +1,3 @@
export * from './SuspensefulImage';
export * from './ColorPickerModal';
export * from './ReorderableList';

View File

@@ -1,14 +1,13 @@
import { useEffect, useState } from 'react';
declare global {
var FocusNavController: any;
}
import { getFocusNavController } from '../utils';
function getQuickAccessWindow(): Window | null {
try {
const context = FocusNavController?.m_ActiveContext || FocusNavController?.m_LastActiveContext;
const navTrees = context?.m_rgGamepadNavigationTrees || FocusNavController?.m_rgGamepadNavigationTrees;
return navTrees?.find((tree: any) => tree?.id === "QuickAccess-NA")?.m_Root?.m_element?.ownerDocument.defaultView ?? null;
const navTrees = getFocusNavController()?.m_rgGamepadNavigationTrees;
return (
navTrees?.find((tree: any) => tree?.id === 'QuickAccess-NA')?.m_Root?.m_element?.ownerDocument.defaultView ?? null
);
} catch (error) {
console.error(error);
return null;

View File

@@ -7,9 +7,10 @@ export interface ButtonItemProps extends ItemProps {
onClick?(e: MouseEvent): void;
disabled?: boolean;
}
export const ButtonItem = Object.values(CommonUIModule).find(
(mod: any) =>
mod?.render?.toString()?.includes('"highlightOnFocus","childrenContainerWidth"') ||
mod?.render?.toString()?.includes('childrenContainerWidth:"min"'),
) as FC<ButtonItemProps>;
export const ButtonItem =
CommonUIModule.ButtonField ||
(Object.values(CommonUIModule).find(
(mod: any) =>
mod?.render?.toString()?.includes('"highlightOnFocus","childrenContainerWidth"') ||
mod?.render?.toString()?.includes('childrenContainerWidth:"min"'),
) as FC<ButtonItemProps>);

View File

@@ -5,7 +5,7 @@ import { findModuleChild } from '../webpack';
export interface PanelSectionProps {
title?: string;
spinner?: boolean;
children?: ReactNode
children?: ReactNode;
}
const [panelSection, mod] = findModuleChild((mod: any) => {
@@ -20,9 +20,9 @@ const [panelSection, mod] = findModuleChild((mod: any) => {
export const PanelSection = panelSection as FC<PanelSectionProps>;
export interface PanelSectionRowProps {
children?: ReactNode
children?: ReactNode;
}
export const PanelSectionRow = Object.values(mod).filter(
(exp: any) => !exp?.toString()?.includes('.PanelSection'),
)[0] as FC<PanelSectionRowProps>;
// New as of Feb 22 2023 Beta || Old
export const PanelSectionRow =
mod.PanelSectionRow ||
(Object.values(mod).filter((exp: any) => !exp?.toString()?.includes('.PanelSection'))[0] as FC<PanelSectionRowProps>);

View File

@@ -77,7 +77,6 @@ export interface WindowRouter {
NavigateToChat(): void;
NavigateToSteamWeb(url: string): void;
NavigateBack(): void;
NavigateToWebRoute(unknown?: any, unknown2?: any): void;
}
export interface WindowStore {
@@ -118,7 +117,6 @@ export interface Navigation {
NavigateToLibraryTab(): void;
NavigateToLayoutPreview(e: unknown): void;
NavigateToSteamWeb(url: string): void;
NavigateToWebRoute(unknown?: any, unknown2?: any): void;
OpenSideMenu(sideMenu: SideMenu): void;
OpenQuickAccessMenu(quickAccessTab?: QuickAccessTab): void;
OpenMainMenu(): void;
@@ -154,33 +152,30 @@ try {
}
}
const newNavigation = {
Navigate: Router.Navigate.bind(Router),
NavigateBack: Router.WindowStore?.GamepadUIMainWindowInstance?.NavigateBack.bind(
Navigate: Router.Navigate?.bind(Router),
NavigateBack: Router.WindowStore?.GamepadUIMainWindowInstance?.NavigateBack?.bind(
Router.WindowStore.GamepadUIMainWindowInstance,
),
NavigateToAppProperties: InternalNavigators?.AppProperties || Router.NavigateToAppProperties.bind(Router),
NavigateToExternalWeb: InternalNavigators?.ExternalWeb || Router.NavigateToExternalWeb.bind(Router),
NavigateToInvites: InternalNavigators?.Invites || Router.NavigateToInvites.bind(Router),
NavigateToChat: Router.NavigateToChat.bind(Router),
NavigateToLibraryTab: InternalNavigators?.LibraryTab || Router.NavigateToLibraryTab.bind(Router),
NavigateToLayoutPreview: Router.NavigateToLayoutPreview.bind(Router),
NavigateToSteamWeb: Router.WindowStore?.GamepadUIMainWindowInstance?.NavigateToSteamWeb.bind(
NavigateToAppProperties: InternalNavigators?.AppProperties || Router.NavigateToAppProperties?.bind(Router),
NavigateToExternalWeb: InternalNavigators?.ExternalWeb || Router.NavigateToExternalWeb?.bind(Router),
NavigateToInvites: InternalNavigators?.Invites || Router.NavigateToInvites?.bind(Router),
NavigateToChat: Router.NavigateToChat?.bind(Router),
NavigateToLibraryTab: InternalNavigators?.LibraryTab || Router.NavigateToLibraryTab?.bind(Router),
NavigateToLayoutPreview: Router.NavigateToLayoutPreview?.bind(Router),
NavigateToSteamWeb: Router.WindowStore?.GamepadUIMainWindowInstance?.NavigateToSteamWeb?.bind(
Router.WindowStore.GamepadUIMainWindowInstance,
),
NavigateToWebRoute: Router.WindowStore?.GamepadUIMainWindowInstance?.NavigateToWebRoute.bind(
Router.WindowStore.GamepadUIMainWindowInstance,
),
OpenSideMenu: Router.WindowStore?.GamepadUIMainWindowInstance?.MenuStore.OpenSideMenu.bind(
OpenSideMenu: Router.WindowStore?.GamepadUIMainWindowInstance?.MenuStore.OpenSideMenu?.bind(
Router.WindowStore.GamepadUIMainWindowInstance.MenuStore,
),
OpenQuickAccessMenu: Router.WindowStore?.GamepadUIMainWindowInstance?.MenuStore.OpenQuickAccessMenu.bind(
OpenQuickAccessMenu: Router.WindowStore?.GamepadUIMainWindowInstance?.MenuStore.OpenQuickAccessMenu?.bind(
Router.WindowStore.GamepadUIMainWindowInstance.MenuStore,
),
OpenMainMenu: Router.WindowStore?.GamepadUIMainWindowInstance?.MenuStore.OpenMainMenu.bind(
OpenMainMenu: Router.WindowStore?.GamepadUIMainWindowInstance?.MenuStore.OpenMainMenu?.bind(
Router.WindowStore.GamepadUIMainWindowInstance.MenuStore,
),
CloseSideMenus: Router.CloseSideMenus.bind(Router),
OpenPowerMenu: Router.OpenPowerMenu.bind(Router),
CloseSideMenus: Router.CloseSideMenus?.bind(Router),
OpenPowerMenu: Router.OpenPowerMenu?.bind(Router),
} as Navigation;
Object.assign(Navigation, newNavigation);

View File

@@ -26,55 +26,56 @@ export * from './Toggle';
export * from './ToggleField';
export * from './SteamClient';
import { SteamClient, SteamAppOverview, LogoPosition } from './SteamClient';
import { AppDetails, LogoPosition, SteamAppOverview, SteamClient } from './SteamClient';
declare global {
var SteamClient: SteamClient;
var SteamClient: SteamClient;
interface Window {
LocalizationManager: {
m_mapTokens: Map<string, string>;
m_mapFallbackTokens: Map<string, string>;
m_rgLocalesToUse: string[];
interface Window {
LocalizationManager: {
m_mapTokens: Map<string, string>;
m_mapFallbackTokens: Map<string, string>;
m_rgLocalesToUse: string[];
};
App: {
m_CurrentUser: {
bIsLimited: boolean;
bIsOfflineMode: boolean;
bSupportAlertActive: boolean;
bCanInviteFriends: boolean;
NotificationCounts: {
comments: number;
inventory_items: number;
invites: number;
gifts: number;
offline_messages: number;
trade_offers: number;
async_game_updates: number;
moderator_messages: number;
help_request_replies: number;
};
App: {
m_CurrentUser: {
bIsLimited: boolean;
bIsOfflineMode: boolean;
bSupportAlertActive: boolean;
bCanInviteFriends: boolean;
NotificationCounts: {
comments: number;
inventory_items: number;
invites: number;
gifts: number;
offline_messages: number;
trade_offers: number;
async_game_updates: number;
moderator_messages: number;
help_request_replies: number;
};
strAccountBalance: string;
strAccountName: string;
strSteamID: string;
};
};
appStore: {
GetAppOverviewByAppID: (appId: number) => SteamAppOverview | null;
GetCustomVerticalCapsuleURLs: (app: SteamAppOverview) => string[];
GetCustomLandcapeImageURLs: (app: SteamAppOverview) => string[];
GetCustomHeroImageURLs: (app: SteamAppOverview) => string[];
GetCustomLogoImageURLs: (app: SteamAppOverview) => string[];
GetLandscapeImageURLForApp: (app: SteamAppOverview) => string;
GetVerticalCapsuleURLForApp: (app: SteamAppOverview) => string;
GetCachedLandscapeImageURLForApp: (app: SteamAppOverview) => string;
GetCachedVerticalImageURLForApp: (app: SteamAppOverview) => string;
GetPregeneratedVerticalCapsuleForApp: (app: SteamAppOverview) => string;
GetIconURLForApp: (app: SteamAppOverview) => string;
};
appDetailsStore: {
GetCustomLogoPosition: (app: SteamAppOverview) => LogoPosition | null;
SaveCustomLogoPosition: (app: SteamAppOverview, logoPositions: LogoPosition) => any;
}
}
strAccountBalance: string;
strAccountName: string;
strSteamID: string;
};
};
appStore: {
GetAppOverviewByAppID: (appId: number) => SteamAppOverview | null;
GetCustomVerticalCapsuleURLs: (app: SteamAppOverview) => string[];
GetCustomLandcapeImageURLs: (app: SteamAppOverview) => string[];
GetCustomHeroImageURLs: (app: SteamAppOverview) => string[];
GetCustomLogoImageURLs: (app: SteamAppOverview) => string[];
GetLandscapeImageURLForApp: (app: SteamAppOverview) => string;
GetVerticalCapsuleURLForApp: (app: SteamAppOverview) => string;
GetCachedLandscapeImageURLForApp: (app: SteamAppOverview) => string;
GetCachedVerticalImageURLForApp: (app: SteamAppOverview) => string;
GetPregeneratedVerticalCapsuleForApp: (app: SteamAppOverview) => string;
GetIconURLForApp: (app: SteamAppOverview) => string;
};
appDetailsStore: {
GetAppDetails: (appId: number) => AppDetails | null;
GetCustomLogoPosition: (app: SteamAppOverview) => LogoPosition | null;
SaveCustomLogoPosition: (app: SteamAppOverview, logoPositions: LogoPosition) => any;
};
}
}

View File

@@ -1,6 +1,11 @@
export * from './patcher';
export * from './react';
declare global {
var FocusNavController: any;
var GamepadNavTree: any;
}
export function joinClassNames(...classes: string[]): string {
return classes.join(' ');
}
@@ -16,7 +21,15 @@ export function findSP(): Window {
// old (SP as host)
if (document.title == 'SP') return window;
// new (SP as popup)
const context = FocusNavController.m_ActiveContext || FocusNavController.m_LastActiveContext;
return context.m_rgGamepadNavigationTrees.find((x: any) => x.m_ID == 'root_1_').Root
.Element.ownerDocument.defaultView;
const focusNav = getFocusNavController();
const context = focusNav.m_ActiveContext || focusNav.m_LastActiveContext;
return context.m_rgGamepadNavigationTrees.find((x: any) => x.m_ID == 'root_1_').Root.Element.ownerDocument
.defaultView;
}
/**
* Gets the correct FocusNavController, as the Feb 22 2023 beta has two for some reason.
*/
export function getFocusNavController(): any {
return window.GamepadNavTree?.m_context?.m_controller || window.FocusNavController;
}