Add routerhook for desktop UI and a basic sidebar menu for Decky in desktop UI

This commit is contained in:
AAGaming
2024-10-04 23:59:53 -04:00
parent 306b0ff8d6
commit 7b32df0948
17 changed files with 442 additions and 175 deletions
+4 -4
View File
@@ -41,7 +41,7 @@ importers:
devDependencies: devDependencies:
'@decky/api': '@decky/api':
specifier: ^1.1.1 specifier: ^1.1.1
version: 1.1.1 version: 1.1.2
'@rollup/plugin-commonjs': '@rollup/plugin-commonjs':
specifier: ^26.0.1 specifier: ^26.0.1
version: 26.0.1(rollup@4.18.0) version: 26.0.1(rollup@4.18.0)
@@ -218,8 +218,8 @@ packages:
resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==} resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@decky/api@1.1.1': '@decky/api@1.1.2':
resolution: {integrity: sha512-R5fkBRHBt5QIQY7Q0AlbVIhlIZ/nTzwBOoi8Rt4Go2fjFnoMKPInCJl6cPjXzimGwl2pyqKJgY6VnH6ar0XrHQ==} resolution: {integrity: sha512-lTMqRpHOrGTCyH2c1jJvkmWhOq2dcnX5/ioHbfCVmyQOBik1OM1BnzF1uROsnNDC5GkRvl3J/ATqYp6vhYpRqw==}
'@decky/ui@4.8.1': '@decky/ui@4.8.1':
resolution: {integrity: sha512-lM4jdeyHjIbxHWxDBhbk+GQvdIT50p6RW9DC+oWSWXlaNWU/iG+8aUAcnfxygFkTP43EkCgjFASsYTfB55CMXA==} resolution: {integrity: sha512-lM4jdeyHjIbxHWxDBhbk+GQvdIT50p6RW9DC+oWSWXlaNWU/iG+8aUAcnfxygFkTP43EkCgjFASsYTfB55CMXA==}
@@ -2817,7 +2817,7 @@ snapshots:
'@babel/helper-validator-identifier': 7.24.7 '@babel/helper-validator-identifier': 7.24.7
to-fast-properties: 2.0.0 to-fast-properties: 2.0.0
'@decky/api@1.1.1': {} '@decky/api@1.1.2': {}
'@decky/ui@4.8.1': {} '@decky/ui@4.8.1': {}
@@ -0,0 +1,72 @@
import { FC, useEffect, useRef, useState } from 'react';
import { useDeckyState } from './DeckyState';
import PluginView from './PluginView';
import { QuickAccessVisibleState } from './QuickAccessVisibleState';
const DeckyDesktopSidebar: FC = () => {
const { desktopMenuOpen, setDesktopMenuOpen } = useDeckyState();
const [closed, setClosed] = useState<boolean>(!desktopMenuOpen);
const [openAnimStart, setOpenAnimStart] = useState<boolean>(desktopMenuOpen);
const closedInterval = useRef<number | null>(null);
useEffect(() => {
const anim = requestAnimationFrame(() => setOpenAnimStart(desktopMenuOpen));
return () => cancelAnimationFrame(anim);
}, [desktopMenuOpen]);
useEffect(() => {
closedInterval.current && clearTimeout(closedInterval.current);
if (desktopMenuOpen) {
setClosed(false);
} else {
closedInterval.current = setTimeout(() => setClosed(true), 500);
}
}, [desktopMenuOpen]);
return (
<>
<div
className="deckyDesktopSidebarDim"
style={{
position: 'absolute',
height: 'calc(100% - 78px - 50px)',
width: '100%',
top: '78px',
left: '0px',
zIndex: 998,
background: 'rgba(0, 0, 0, 0.7)',
opacity: openAnimStart ? 1 : 0,
display: desktopMenuOpen || !closed ? 'flex' : 'none',
transition: 'opacity 0.4s cubic-bezier(0.65, 0, 0.35, 1)',
}}
onClick={() => setDesktopMenuOpen(false)}
/>
<div
className="deckyDesktopSidebar"
style={{
position: 'absolute',
height: 'calc(100% - 78px - 50px)',
width: '350px',
paddingLeft: '16px',
top: '78px',
right: '0px',
zIndex: 999,
transition: 'transform 0.4s cubic-bezier(0.65, 0, 0.35, 1)',
transform: openAnimStart ? 'translateX(0px)' : 'translateX(366px)',
overflowY: 'scroll',
// prevents chromium border jank
display: desktopMenuOpen || !closed ? 'flex' : 'none',
flexDirection: 'column',
background: '#171d25',
}}
>
<QuickAccessVisibleState.Provider value={desktopMenuOpen || !closed}>
<PluginView desktop={true} />
</QuickAccessVisibleState.Provider>
</div>
</>
);
};
export default DeckyDesktopSidebar;
@@ -0,0 +1,44 @@
import { CSSProperties, FC } from 'react';
import DeckyDesktopSidebar from './DeckyDesktopSidebar';
import DeckyIcon from './DeckyIcon';
import { useDeckyState } from './DeckyState';
const DeckyDesktopUI: FC = () => {
const { desktopMenuOpen, setDesktopMenuOpen } = useDeckyState();
return (
<>
<style>
{`
.deckyDesktopIcon {
color: #67707b;
}
.deckyDesktopIcon:hover {
color: #fff;
}
`}
</style>
<DeckyIcon
className="deckyDesktopIcon"
width={24}
height={24}
onClick={() => setDesktopMenuOpen(!desktopMenuOpen)}
style={
{
position: 'absolute',
top: '36px', // nav text is 34px but 36px looks nicer to me
right: '10px', // <- is 16px but 10px looks nicer to me
width: '24px',
height: '24px',
cursor: 'pointer',
transition: 'color 0.3s linear',
'-webkit-app-region': 'no-drag',
} as CSSProperties
}
/>
<DeckyDesktopSidebar />
</>
);
};
export default DeckyDesktopUI;
@@ -1,12 +1,17 @@
import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react'; import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
import { UIMode } from '../enums';
interface PublicDeckyGlobalComponentsState { interface PublicDeckyGlobalComponentsState {
components: Map<string, FC>; components: Map<UIMode, Map<string, FC>>;
} }
export class DeckyGlobalComponentsState { export class DeckyGlobalComponentsState {
// TODO a set would be better // TODO a set would be better
private _components = new Map<string, FC>(); private _components = new Map<UIMode, Map<string, FC>>([
[UIMode.BigPicture, new Map()],
[UIMode.Desktop, new Map()],
]);
public eventBus = new EventTarget(); public eventBus = new EventTarget();
@@ -14,13 +19,19 @@ export class DeckyGlobalComponentsState {
return { components: this._components }; return { components: this._components };
} }
addComponent(path: string, component: FC) { addComponent(path: string, component: FC, uiMode: UIMode) {
this._components.set(path, component); const components = this._components.get(uiMode);
if (!components) throw new Error(`UI mode ${uiMode} not supported.`);
components.set(path, component);
this.notifyUpdate(); this.notifyUpdate();
} }
removeComponent(path: string) { removeComponent(path: string, uiMode: UIMode) {
this._components.delete(path); const components = this._components.get(uiMode);
if (!components) throw new Error(`UI mode ${uiMode} not supported.`);
components.delete(path);
this.notifyUpdate(); this.notifyUpdate();
} }
@@ -30,8 +41,8 @@ export class DeckyGlobalComponentsState {
} }
interface DeckyGlobalComponentsContext extends PublicDeckyGlobalComponentsState { interface DeckyGlobalComponentsContext extends PublicDeckyGlobalComponentsState {
addComponent(path: string, component: FC): void; addComponent(path: string, component: FC, uiMode: UIMode): void;
removeComponent(path: string): void; removeComponent(path: string, uiMode: UIMode): void;
} }
const DeckyGlobalComponentsContext = createContext<DeckyGlobalComponentsContext>(null as any); const DeckyGlobalComponentsContext = createContext<DeckyGlobalComponentsContext>(null as any);
+20 -10
View File
@@ -1,6 +1,8 @@
import { ComponentType, FC, ReactNode, createContext, useContext, useEffect, useState } from 'react'; import { ComponentType, FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
import type { RouteProps } from 'react-router'; import type { RouteProps } from 'react-router';
import { UIMode } from '../enums';
export interface RouterEntry { export interface RouterEntry {
props: Omit<RouteProps, 'path' | 'children'>; props: Omit<RouteProps, 'path' | 'children'>;
component: ComponentType; component: ComponentType;
@@ -10,12 +12,16 @@ export type RoutePatch = (route: RouteProps) => RouteProps;
interface PublicDeckyRouterState { interface PublicDeckyRouterState {
routes: Map<string, RouterEntry>; routes: Map<string, RouterEntry>;
routePatches: Map<string, Set<RoutePatch>>; routePatches: Map<UIMode, Map<string, Set<RoutePatch>>>;
} }
export class DeckyRouterState { export class DeckyRouterState {
private _routes = new Map<string, RouterEntry>(); private _routes = new Map<string, RouterEntry>();
private _routePatches = new Map<string, Set<RoutePatch>>(); // Update when support for new UIModes is added
private _routePatches = new Map<UIMode, Map<string, Set<RoutePatch>>>([
[UIMode.BigPicture, new Map()],
[UIMode.Desktop, new Map()],
]);
public eventBus = new EventTarget(); public eventBus = new EventTarget();
@@ -28,22 +34,26 @@ export class DeckyRouterState {
this.notifyUpdate(); this.notifyUpdate();
} }
addPatch(path: string, patch: RoutePatch) { addPatch(path: string, patch: RoutePatch, uiMode: UIMode) {
let patchList = this._routePatches.get(path); const patchesForMode = this._routePatches.get(uiMode);
if (!patchesForMode) throw new Error(`UI mode ${uiMode} not supported.`);
let patchList = patchesForMode.get(path);
if (!patchList) { if (!patchList) {
patchList = new Set(); patchList = new Set();
this._routePatches.set(path, patchList); patchesForMode.set(path, patchList);
} }
patchList.add(patch); patchList.add(patch);
this.notifyUpdate(); this.notifyUpdate();
return patch; return patch;
} }
removePatch(path: string, patch: RoutePatch) { removePatch(path: string, patch: RoutePatch, uiMode: UIMode) {
const patchList = this._routePatches.get(path); const patchesForMode = this._routePatches.get(uiMode);
if (!patchesForMode) throw new Error(`UI mode ${uiMode} not supported.`);
const patchList = patchesForMode.get(path);
patchList?.delete(patch); patchList?.delete(patch);
if (patchList?.size == 0) { if (patchList?.size == 0) {
this._routePatches.delete(path); patchesForMode.delete(path);
} }
this.notifyUpdate(); this.notifyUpdate();
} }
@@ -60,8 +70,8 @@ export class DeckyRouterState {
interface DeckyRouterStateContext extends PublicDeckyRouterState { interface DeckyRouterStateContext extends PublicDeckyRouterState {
addRoute(path: string, component: RouterEntry['component'], props: RouterEntry['props']): void; addRoute(path: string, component: RouterEntry['component'], props: RouterEntry['props']): void;
addPatch(path: string, patch: RoutePatch): RoutePatch; addPatch(path: string, patch: RoutePatch, uiMode?: UIMode): RoutePatch;
removePatch(path: string, patch: RoutePatch): void; removePatch(path: string, patch: RoutePatch, uiMode?: UIMode): void;
removeRoute(path: string): void; removeRoute(path: string): void;
} }
+11
View File
@@ -17,6 +17,7 @@ interface PublicDeckyState {
versionInfo: VerInfo | null; versionInfo: VerInfo | null;
notificationSettings: NotificationSettings; notificationSettings: NotificationSettings;
userInfo: UserInfo | null; userInfo: UserInfo | null;
desktopMenuOpen: boolean;
} }
export interface UserInfo { export interface UserInfo {
@@ -36,6 +37,7 @@ export class DeckyState {
private _versionInfo: VerInfo | null = null; private _versionInfo: VerInfo | null = null;
private _notificationSettings = DEFAULT_NOTIFICATION_SETTINGS; private _notificationSettings = DEFAULT_NOTIFICATION_SETTINGS;
private _userInfo: UserInfo | null = null; private _userInfo: UserInfo | null = null;
private _desktopMenuOpen: boolean = false;
public eventBus = new EventTarget(); public eventBus = new EventTarget();
@@ -52,6 +54,7 @@ export class DeckyState {
versionInfo: this._versionInfo, versionInfo: this._versionInfo,
notificationSettings: this._notificationSettings, notificationSettings: this._notificationSettings,
userInfo: this._userInfo, userInfo: this._userInfo,
desktopMenuOpen: this._desktopMenuOpen,
}; };
} }
@@ -115,6 +118,11 @@ export class DeckyState {
this.notifyUpdate(); this.notifyUpdate();
} }
setDesktopMenuOpen(open: boolean) {
this._desktopMenuOpen = open;
this.notifyUpdate();
}
private notifyUpdate() { private notifyUpdate() {
this.eventBus.dispatchEvent(new Event('update')); this.eventBus.dispatchEvent(new Event('update'));
} }
@@ -126,6 +134,7 @@ interface DeckyStateContext extends PublicDeckyState {
setActivePlugin(name: string): void; setActivePlugin(name: string): void;
setPluginOrder(pluginOrder: string[]): void; setPluginOrder(pluginOrder: string[]): void;
closeActivePlugin(): void; closeActivePlugin(): void;
setDesktopMenuOpen(open: boolean): void;
} }
const DeckyStateContext = createContext<DeckyStateContext>(null as any); const DeckyStateContext = createContext<DeckyStateContext>(null as any);
@@ -155,6 +164,7 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
const setActivePlugin = deckyState.setActivePlugin.bind(deckyState); const setActivePlugin = deckyState.setActivePlugin.bind(deckyState);
const closeActivePlugin = deckyState.closeActivePlugin.bind(deckyState); const closeActivePlugin = deckyState.closeActivePlugin.bind(deckyState);
const setPluginOrder = deckyState.setPluginOrder.bind(deckyState); const setPluginOrder = deckyState.setPluginOrder.bind(deckyState);
const setDesktopMenuOpen = deckyState.setDesktopMenuOpen.bind(deckyState);
return ( return (
<DeckyStateContext.Provider <DeckyStateContext.Provider
@@ -165,6 +175,7 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
setActivePlugin, setActivePlugin,
closeActivePlugin, closeActivePlugin,
setPluginOrder, setPluginOrder,
setDesktopMenuOpen,
}} }}
> >
{children} {children}
+5
View File
@@ -24,6 +24,11 @@ const Markdown: FunctionComponent<MarkdownProps> = (props) => {
props.onDismiss?.(); props.onDismiss?.();
Navigation.NavigateToExternalWeb(aRef.current!.href); Navigation.NavigateToExternalWeb(aRef.current!.href);
}} }}
onClick={(e) => {
e.preventDefault();
props.onDismiss?.();
Navigation.NavigateToExternalWeb(aRef.current!.href);
}}
style={{ display: 'inline' }} style={{ display: 'inline' }}
> >
<a ref={aRef} {...nodeProps.node.properties}> <a ref={aRef} {...nodeProps.node.properties}>
+7 -3
View File
@@ -9,7 +9,11 @@ import NotificationBadge from './NotificationBadge';
import { useQuickAccessVisible } from './QuickAccessVisibleState'; import { useQuickAccessVisible } from './QuickAccessVisibleState';
import TitleView from './TitleView'; import TitleView from './TitleView';
const PluginView: FC = () => { interface PluginViewProps {
desktop?: boolean;
}
const PluginView: FC<PluginViewProps> = ({ desktop = false }) => {
const { hiddenPlugins } = useDeckyState(); const { hiddenPlugins } = useDeckyState();
const { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState(); const { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState();
const visible = useQuickAccessVisible(); const visible = useQuickAccessVisible();
@@ -27,7 +31,7 @@ const PluginView: FC = () => {
if (activePlugin) { if (activePlugin) {
return ( return (
<Focusable onCancelButton={closeActivePlugin}> <Focusable onCancelButton={closeActivePlugin}>
<TitleView /> <TitleView desktop={desktop} />
<div style={{ height: '100%', paddingTop: '16px' }}> <div style={{ height: '100%', paddingTop: '16px' }}>
<ErrorBoundary>{(visible || activePlugin.alwaysRender) && activePlugin.content}</ErrorBoundary> <ErrorBoundary>{(visible || activePlugin.alwaysRender) && activePlugin.content}</ErrorBoundary>
</div> </div>
@@ -36,7 +40,7 @@ const PluginView: FC = () => {
} }
return ( return (
<> <>
<TitleView /> <TitleView desktop={desktop} />
<div <div
style={{ style={{
paddingTop: '16px', paddingTop: '16px',
@@ -1,6 +1,6 @@
import { FC, ReactNode, createContext, useContext, useState } from 'react'; import { FC, ReactNode, createContext, useContext, useState } from 'react';
const QuickAccessVisibleState = createContext<boolean>(false); export const QuickAccessVisibleState = createContext<boolean>(false);
export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState); export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState);
+21 -8
View File
@@ -14,18 +14,34 @@ const titleStyles: CSSProperties = {
top: '0px', top: '0px',
}; };
const TitleView: FC = () => { interface TitleViewProps {
const { activePlugin, closeActivePlugin } = useDeckyState(); desktop?: boolean;
}
const TitleView: FC<TitleViewProps> = ({ desktop }) => {
const { activePlugin, closeActivePlugin, setDesktopMenuOpen } = useDeckyState();
const { t } = useTranslation(); const { t } = useTranslation();
const onSettingsClick = () => { const onSettingsClick = () => {
Navigation.Navigate('/decky/settings'); Navigation.Navigate('/decky/settings');
Navigation.CloseSideMenus(); Navigation.CloseSideMenus();
setDesktopMenuOpen(false);
}; };
const onStoreClick = () => { const onStoreClick = () => {
Navigation.Navigate('/decky/store'); Navigation.Navigate('/decky/store');
Navigation.CloseSideMenus(); Navigation.CloseSideMenus();
setDesktopMenuOpen(false);
};
const buttonStyles = {
height: '28px',
width: '40px',
minWidth: 0,
padding: desktop ? '' : '10px 12px',
display: 'flex',
alignItems: desktop ? 'center' : '',
justifyContent: desktop ? 'center' : '',
}; };
if (activePlugin === null) { if (activePlugin === null) {
@@ -33,14 +49,14 @@ const TitleView: FC = () => {
<Focusable style={titleStyles} className={staticClasses.Title}> <Focusable style={titleStyles} className={staticClasses.Title}>
<div style={{ marginRight: 'auto', flex: 0.9 }}>Decky</div> <div style={{ marginRight: 'auto', flex: 0.9 }}>Decky</div>
<DialogButton <DialogButton
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }} style={buttonStyles}
onClick={onStoreClick} onClick={onStoreClick}
onOKActionDescription={t('TitleView.decky_store_desc')} onOKActionDescription={t('TitleView.decky_store_desc')}
> >
<FaStore style={{ marginTop: '-4px', display: 'block' }} /> <FaStore style={{ marginTop: '-4px', display: 'block' }} />
</DialogButton> </DialogButton>
<DialogButton <DialogButton
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }} style={buttonStyles}
onClick={onSettingsClick} onClick={onSettingsClick}
onOKActionDescription={t('TitleView.settings_desc')} onOKActionDescription={t('TitleView.settings_desc')}
> >
@@ -52,10 +68,7 @@ const TitleView: FC = () => {
return ( return (
<Focusable className={staticClasses.Title} style={titleStyles}> <Focusable className={staticClasses.Title} style={titleStyles}>
<DialogButton <DialogButton style={buttonStyles} onClick={closeActivePlugin}>
style={{ height: '28px', width: '40px', minWidth: 0, padding: '10px 12px' }}
onClick={closeActivePlugin}
>
<FaArrowLeft style={{ marginTop: '-4px', display: 'block' }} /> <FaArrowLeft style={{ marginTop: '-4px', display: 'block' }} />
</DialogButton> </DialogButton>
{activePlugin?.titleView || <div style={{ flex: 0.9 }}>{activePlugin.name}</div>} {activePlugin?.titleView || <div style={{ flex: 0.9 }}>{activePlugin.name}</div>}
@@ -80,7 +80,10 @@ const MultiplePluginsInstallModal: FC<MultiplePluginsInstallModalProps> = ({
onOK={async () => { onOK={async () => {
setLoading(true); setLoading(true);
await onOK(); await onOK();
setTimeout(() => Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky), 250); setTimeout(() => {
Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky);
DeckyPluginLoader.setDesktopMenuOpen(true);
}, 250);
setTimeout(() => DeckyPluginLoader.checkPluginUpdates(), 1000); setTimeout(() => DeckyPluginLoader.checkPluginUpdates(), 1000);
}} }}
onCancel={async () => { onCancel={async () => {
@@ -51,7 +51,10 @@ const PluginInstallModal: FC<PluginInstallModalProps> = ({
onOK={async () => { onOK={async () => {
setLoading(true); setLoading(true);
await onOK(); await onOK();
setTimeout(() => Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky), 250); setTimeout(() => {
Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky);
DeckyPluginLoader.setDesktopMenuOpen(true);
}, 250);
setTimeout(() => DeckyPluginLoader.checkPluginUpdates(), 1000); setTimeout(() => DeckyPluginLoader.checkPluginUpdates(), 1000);
}} }}
onCancel={async () => { onCancel={async () => {
+16 -1
View File
@@ -53,5 +53,20 @@ export default function SettingsPage() {
}, },
]; ];
return <SidebarNavigation pages={pages} />; return (
<div className="deckySettingsHeightHack">
<style>
{/* hacky fix to work around height: 720px in desktop ui */}
{`
.deckySettingsHeightHack {
height: 100% !important;
}
.deckySettingsHeightHack > div {
height: 100% !important;
}
`}
</style>
<SidebarNavigation pages={pages} />
</div>
);
} }
@@ -6,8 +6,8 @@ import {
Focusable, Focusable,
ProgressBarWithInfo, ProgressBarWithInfo,
Spinner, Spinner,
findSP,
showModal, showModal,
useWindowRef,
} from '@decky/ui'; } from '@decky/ui';
import { Suspense, lazy, useCallback, useEffect, useState } from 'react'; import { Suspense, lazy, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -21,45 +21,48 @@ import WithSuspense from '../../../WithSuspense';
const MarkdownRenderer = lazy(() => import('../../../Markdown')); const MarkdownRenderer = lazy(() => import('../../../Markdown'));
function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | null; closeModal?: () => {} }) { function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | null; closeModal?: () => {} }) {
const SP = findSP(); const [outerRef, win] = useWindowRef<HTMLDivElement>();
const { t } = useTranslation(); const { t } = useTranslation();
// TODO proper desktop scrolling
return ( return (
<Focusable onCancelButton={closeModal}> <Focusable ref={outerRef} onCancelButton={closeModal}>
<FocusRing> <FocusRing>
<Carousel {win && (
fnItemRenderer={(id: number) => ( <Carousel
<Focusable fnItemRenderer={(id: number) => (
style={{ <Focusable
marginTop: '40px', style={{
height: 'calc( 100% - 40px )', marginTop: '40px',
overflowY: 'scroll', height: 'calc( 100% - 40px )',
display: 'flex', overflowY: 'scroll',
justifyContent: 'center', display: 'flex',
margin: '40px', justifyContent: 'center',
}} margin: '40px',
> }}
<div> >
<h1>{versionInfo?.all?.[id]?.name || 'Invalid Update Name'}</h1> <div>
{versionInfo?.all?.[id]?.body ? ( <h1>{versionInfo?.all?.[id]?.name || 'Invalid Update Name'}</h1>
<WithSuspense> {versionInfo?.all?.[id]?.body ? (
<MarkdownRenderer onDismiss={closeModal}>{versionInfo.all[id].body}</MarkdownRenderer> <WithSuspense>
</WithSuspense> <MarkdownRenderer onDismiss={closeModal}>{versionInfo.all[id].body}</MarkdownRenderer>
) : ( </WithSuspense>
t('Updater.no_patch_notes_desc') ) : (
)} t('Updater.no_patch_notes_desc')
</div> )}
</Focusable> </div>
)} </Focusable>
fnGetId={(id) => id} )}
nNumItems={versionInfo?.all?.length} fnGetId={(id) => id}
nHeight={SP.innerHeight - 40} nNumItems={versionInfo?.all?.length}
nItemHeight={SP.innerHeight - 40} nHeight={(win?.innerHeight || 800) - 40}
nItemMarginX={0} nItemHeight={(win?.innerHeight || 800) - 40}
initialColumn={0} nItemMarginX={0}
autoFocus={true} initialColumn={0}
fnGetColumnWidth={() => SP.innerWidth} autoFocus={true}
name={t('Updater.decky_updates') as string} fnGetColumnWidth={() => win?.innerHeight || 1280}
/> name={t('Updater.decky_updates') as string}
/>
)}
</FocusRing> </FocusRing>
</Focusable> </Focusable>
); );
@@ -72,6 +75,8 @@ export default function UpdaterSettings() {
const [updateProgress, setUpdateProgress] = useState<number>(-1); const [updateProgress, setUpdateProgress] = useState<number>(-1);
const [reloading, setReloading] = useState<boolean>(false); const [reloading, setReloading] = useState<boolean>(false);
const [windowRef, win] = useWindowRef<HTMLDivElement>();
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
@@ -91,11 +96,12 @@ export default function UpdaterSettings() {
}, []); }, []);
const showPatchNotes = useCallback(() => { const showPatchNotes = useCallback(() => {
showModal(<PatchNotesModal versionInfo={versionInfo} />); // TODO set width and height on desktop - needs fixing in DFL?
}, [versionInfo]); showModal(<PatchNotesModal versionInfo={versionInfo} />, win!);
}, [versionInfo, win]);
return ( return (
<> <div ref={windowRef}>
<Field <Field
onOptionsActionDescription={versionInfo?.all ? t('Updater.patch_notes_desc') : undefined} onOptionsActionDescription={versionInfo?.all ? t('Updater.patch_notes_desc') : undefined}
onOptionsButton={versionInfo?.all ? showPatchNotes : undefined} onOptionsButton={versionInfo?.all ? showPatchNotes : undefined}
@@ -164,6 +170,6 @@ export default function UpdaterSettings() {
</Suspense> </Suspense>
</InlinePatchNotes> </InlinePatchNotes>
)} )}
</> </div>
); );
} }
+4
View File
@@ -0,0 +1,4 @@
export enum UIMode {
BigPicture = 4,
Desktop = 7,
}
+35 -5
View File
@@ -1,4 +1,4 @@
import { ToastNotification } from '@decky/api'; import type { ToastNotification } from '@decky/api';
import { import {
ModalRoot, ModalRoot,
Navigation, Navigation,
@@ -13,6 +13,7 @@ import {
import { FC, lazy } from 'react'; import { FC, lazy } from 'react';
import { FaDownload, FaExclamationCircle, FaPlug } from 'react-icons/fa'; import { FaDownload, FaExclamationCircle, FaPlug } from 'react-icons/fa';
import DeckyDesktopUI from './components/DeckyDesktopUI';
import DeckyIcon from './components/DeckyIcon'; import DeckyIcon from './components/DeckyIcon';
import { DeckyState, DeckyStateContextProvider, UserInfo, useDeckyState } from './components/DeckyState'; import { DeckyState, DeckyStateContextProvider, UserInfo, useDeckyState } from './components/DeckyState';
import { File, FileSelectionType } from './components/modals/filepicker'; import { File, FileSelectionType } from './components/modals/filepicker';
@@ -24,13 +25,14 @@ import NotificationBadge from './components/NotificationBadge';
import PluginView from './components/PluginView'; import PluginView from './components/PluginView';
import { useQuickAccessVisible } from './components/QuickAccessVisibleState'; import { useQuickAccessVisible } from './components/QuickAccessVisibleState';
import WithSuspense from './components/WithSuspense'; import WithSuspense from './components/WithSuspense';
import { UIMode } from './enums';
import ErrorBoundaryHook from './errorboundary-hook'; import ErrorBoundaryHook from './errorboundary-hook';
import { FrozenPluginService } from './frozen-plugins-service'; import { FrozenPluginService } from './frozen-plugins-service';
import { HiddenPluginsService } from './hidden-plugins-service'; import { HiddenPluginsService } from './hidden-plugins-service';
import Logger from './logger'; import Logger from './logger';
import { NotificationService } from './notification-service'; import { NotificationService } from './notification-service';
import { InstallType, Plugin, PluginLoadType } from './plugin'; import { InstallType, Plugin, PluginLoadType } from './plugin';
import RouterHook, { UIMode } from './router-hook'; import RouterHook from './router-hook';
import { deinitSteamFixes, initSteamFixes } from './steamfixes'; import { deinitSteamFixes, initSteamFixes } from './steamfixes';
import { checkForPluginUpdates } from './store'; import { checkForPluginUpdates } from './store';
import TabsHook from './tabs-hook'; import TabsHook from './tabs-hook';
@@ -160,6 +162,21 @@ class PluginLoader extends Logger {
); );
}); });
// needs the 1s wait or the entire app becomes drag target lol
sleep(1000).then(() =>
this.routerHook.addGlobalComponent(
'DeckyDesktopUI',
() => {
return (
<DeckyStateContextProvider deckyState={this.deckyState}>
<DeckyDesktopUI />
</DeckyStateContextProvider>
);
},
UIMode.Desktop,
),
);
initSteamFixes(); initSteamFixes();
initFilepickerPatches(); initFilepickerPatches();
@@ -362,6 +379,7 @@ class PluginLoader extends Logger {
public deinit() { public deinit() {
this.routerHook.removeRoute('/decky/store'); this.routerHook.removeRoute('/decky/store');
this.routerHook.removeRoute('/decky/settings'); this.routerHook.removeRoute('/decky/settings');
this.routerHook.removeGlobalComponent('DeckyDesktopUI', UIMode.Desktop);
deinitSteamFixes(); deinitSteamFixes();
deinitFilepickerPatches(); deinitFilepickerPatches();
this.routerHook.deinit(); this.routerHook.deinit();
@@ -627,8 +645,8 @@ class PluginLoader extends Logger {
// Things will break *very* badly if plugin code touches this outside of @decky/api, so lets make that clear. // Things will break *very* badly if plugin code touches this outside of @decky/api, so lets make that clear.
window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit = { window.__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit = {
connect: (version: number, pluginName: string) => { connect: (version: number, pluginName: string) => {
if (version < 1 || version > 2) { if (version < 1 || version > 3) {
console.warn(`Plugin ${pluginName} requested unsupported api version ${version}.`); console.warn(`Plugin ${pluginName} requested unsupported API version ${version}.`);
} }
const eventListeners: listenerMap = new Map(); const eventListeners: listenerMap = new Map();
@@ -671,12 +689,20 @@ class PluginLoader extends Logger {
_version: 1, _version: 1,
} as any; } as any;
// adds useQuickAccessVisible
if (version >= 2) { if (version >= 2) {
backendAPI._version = 2; backendAPI._version = 2;
backendAPI.useQuickAccessVisible = useQuickAccessVisible; backendAPI.useQuickAccessVisible = useQuickAccessVisible;
} }
this.debug(`${pluginName} connected to loader API.`); // adds uiMode param to route patching and global component functions. no functional changes, but we should warn anyway.
if (version >= 3) {
backendAPI._version = 3;
}
this.debug(
`${pluginName} connected to loader API version ${backendAPI._version} (requested version ${version}).`,
);
return backendAPI; return backendAPI;
}, },
}; };
@@ -733,6 +759,10 @@ class PluginLoader extends Logger {
return pluginAPI; return pluginAPI;
} }
public setDesktopMenuOpen(open: boolean) {
this.deckyState.setDesktopMenuOpen(open);
}
} }
export default PluginLoader; export default PluginLoader;
+128 -92
View File
@@ -6,7 +6,9 @@ import {
findInTree, findInTree,
findModuleByExport, findModuleByExport,
getReactRoot, getReactRoot,
injectFCTrampoline,
sleep, sleep,
wrapReactType,
} from '@decky/ui'; } from '@decky/ui';
import { FC, ReactElement, ReactNode, cloneElement, createElement } from 'react'; import { FC, ReactElement, ReactNode, cloneElement, createElement } from 'react';
import type { Route } from 'react-router'; import type { Route } from 'react-router';
@@ -23,6 +25,7 @@ import {
RouterEntry, RouterEntry,
useDeckyRouterState, useDeckyRouterState,
} from './components/DeckyRouterState'; } from './components/DeckyRouterState';
import { UIMode } from './enums';
import Logger from './logger'; import Logger from './logger';
declare global { declare global {
@@ -31,18 +34,18 @@ declare global {
} }
} }
export enum UIMode {
BigPicture = 4,
Desktop = 7,
}
const isPatched = Symbol('is patched'); const isPatched = Symbol('is patched');
class RouterHook extends Logger { class RouterHook extends Logger {
private routerState: DeckyRouterState = new DeckyRouterState(); private routerState: DeckyRouterState = new DeckyRouterState();
private globalComponentsState: DeckyGlobalComponentsState = new DeckyGlobalComponentsState(); private globalComponentsState: DeckyGlobalComponentsState = new DeckyGlobalComponentsState();
private renderedComponents: ReactElement[] = []; private renderedComponents = new Map<UIMode, ReactElement[]>([
[UIMode.BigPicture, []],
[UIMode.Desktop, []],
]);
private Route: any; private Route: any;
private DesktopRoute: any;
private wrappedDesktopLibraryMemo?: any;
private DeckyGamepadRouterWrapper = this.gamepadRouterWrapper.bind(this); private DeckyGamepadRouterWrapper = this.gamepadRouterWrapper.bind(this);
private DeckyDesktopRouterWrapper = this.desktopRouterWrapper.bind(this); private DeckyDesktopRouterWrapper = this.desktopRouterWrapper.bind(this);
private DeckyGlobalComponentsWrapper = this.globalComponentsWrapper.bind(this); private DeckyGlobalComponentsWrapper = this.globalComponentsWrapper.bind(this);
@@ -76,6 +79,21 @@ class RouterHook extends Logger {
this.error('Failed to find router stack module'); this.error('Failed to find router stack module');
} }
const routerModule = findModuleByExport((e) => e?.displayName == 'Router');
if (routerModule) {
this.DesktopRoute = Object.values(routerModule).find(
(e) =>
typeof e == 'function' &&
e?.prototype?.render?.toString()?.includes('props.computedMatch') &&
e?.prototype?.render?.toString()?.includes('.Children.count('),
);
if (!this.DesktopRoute) {
this.error('Failed to find DesktopRoute component');
}
} else {
this.error('Failed to find router module, desktop routes will not work');
}
this.modeChangeRegistration = SteamClient.UI.RegisterForUIModeChanged((mode: UIMode) => { this.modeChangeRegistration = SteamClient.UI.RegisterForUIModeChanged((mode: UIMode) => {
this.debug(`UI mode changed to ${mode}`); this.debug(`UI mode changed to ${mode}`);
if (this.patchedModes.has(mode)) return; if (this.patchedModes.has(mode)) return;
@@ -88,7 +106,7 @@ class RouterHook extends Logger {
break; break;
// Not fully implemented yet // Not fully implemented yet
case UIMode.Desktop: case UIMode.Desktop:
this.debug("Patching desktop router"); this.debug('Patching desktop router');
this.patchDesktopRouter(); this.patchDesktopRouter();
break; break;
default: default:
@@ -109,7 +127,7 @@ class RouterHook extends Logger {
await this.waitForUnlock(); await this.waitForUnlock();
let routerNode = findRouterNode(); let routerNode = findRouterNode();
while (!routerNode) { while (!routerNode) {
this.warn('Failed to find Router node, reattempting in 5 seconds.'); this.warn('Failed to find GamepadUI Router node, reattempting in 5 seconds.');
await sleep(5000); await sleep(5000);
await this.waitForUnlock(); await this.waitForUnlock();
routerNode = findRouterNode(); routerNode = findRouterNode();
@@ -130,49 +148,34 @@ class RouterHook extends Logger {
} }
} }
// Currently unused
private async patchDesktopRouter() { private async patchDesktopRouter() {
const root = getReactRoot(document.getElementById('root') as any); const root = getReactRoot(document.getElementById('root') as any);
const findRouterNode = () => const findRouterNode = () =>
findInReactTree(root, (node) => node?.elementType?.type?.toString?.()?.includes('bShowDesktopUIContent:')); findInReactTree(root, (node) => {
const typeStr = node?.elementType?.toString?.();
return (
typeStr &&
typeStr?.includes('.IsMainDesktopWindow') &&
typeStr?.includes('.IN_STEAMUI_SHARED_CONTEXT') &&
typeStr?.includes('.ContentFrame') &&
typeStr?.includes('.Console()')
);
});
let routerNode = findRouterNode(); let routerNode = findRouterNode();
while (!routerNode) { while (!routerNode) {
this.warn('Failed to find Router node, reattempting in 5 seconds.'); this.warn('Failed to find DesktopUI Router node, reattempting in 5 seconds.');
await sleep(5000); await sleep(5000);
routerNode = findRouterNode(); routerNode = findRouterNode();
} }
if (routerNode) { if (routerNode) {
// this.debug("desktop router node", routerNode);
// Patch the component globally // Patch the component globally
this.desktopRouterPatch = afterPatch(routerNode.elementType, 'type', this.handleDesktopRouterRender.bind(this)); const patchedRenderer = injectFCTrampoline(routerNode.elementType);
// Swap out the current instance this.desktopRouterPatch = afterPatch(patchedRenderer, 'component', this.handleDesktopRouterRender.bind(this));
routerNode.type = routerNode.elementType.type;
if (routerNode?.alternate) {
routerNode.alternate.type = routerNode.type;
}
// Force a full rerender via our custom error boundary // Force a full rerender via our custom error boundary
const errorBoundaryNode = findInTree(routerNode, (e) => e?.stateNode?._deckyForceRerender, { const errorBoundaryNode = findInTree(routerNode, (e) => e?.stateNode?._deckyForceRerender, {
walkable: ['return'], walkable: ['return'],
}); });
errorBoundaryNode?.stateNode?._deckyForceRerender?.(); errorBoundaryNode?.stateNode?._deckyForceRerender?.();
// this.debug("desktop router node", routerNode);
// // Patch the component globally
// this.desktopRouterPatch = afterPatch(routerNode.type.prototype, 'render', this.handleDesktopRouterRender.bind(this));
// const stateNodeClone = { render: routerNode.stateNode.render } as any;
// // Patch the current instance. render is readonly so we have to do this.
// Object.assign(stateNodeClone, routerNode.stateNode);
// Object.setPrototypeOf(stateNodeClone, Object.getPrototypeOf(routerNode.stateNode));
// this.desktopRouterFirstInstancePatch = afterPatch(stateNodeClone, 'render', this.handleDesktopRouterRender.bind(this));
// routerNode.stateNode = stateNodeClone;
// // Swap out the current instance
// if (routerNode?.alternate) {
// routerNode.alternate.type = routerNode.type;
// routerNode.alternate.stateNode = routerNode.stateNode;
// }
// routerNode.stateNode.forceUpdate();
// Force a full rerender via our custom error boundary
// const errorBoundaryNode = findInTree(routerNode, e => e?.stateNode?._deckyForceRerender, { walkable: ["return"] });
// errorBoundaryNode?.stateNode?._deckyForceRerender?.();
} }
} }
@@ -196,10 +199,18 @@ class RouterHook extends Logger {
const returnVal = ( const returnVal = (
<> <>
<DeckyRouterStateContextProvider deckyRouterState={this.routerState}> <DeckyRouterStateContextProvider deckyRouterState={this.routerState}>
<style>
{`
.deckyDesktopDialogPaddingHack + * .DialogContent_InnerWidth {
max-width: unset !important;
}
`}
</style>
<div className="deckyDesktopDialogPaddingHack" />
<DeckyDesktopRouterWrapper>{ret}</DeckyDesktopRouterWrapper> <DeckyDesktopRouterWrapper>{ret}</DeckyDesktopRouterWrapper>
</DeckyRouterStateContextProvider> </DeckyRouterStateContextProvider>
<DeckyGlobalComponentsStateContextProvider deckyGlobalComponentsState={this.globalComponentsState}> <DeckyGlobalComponentsStateContextProvider deckyGlobalComponentsState={this.globalComponentsState}>
<DeckyGlobalComponentsWrapper /> <DeckyGlobalComponentsWrapper uiMode={UIMode.Desktop} />
</DeckyGlobalComponentsStateContextProvider> </DeckyGlobalComponentsStateContextProvider>
</> </>
); );
@@ -219,7 +230,7 @@ class RouterHook extends Logger {
<DeckyGamepadRouterWrapper>{ret}</DeckyGamepadRouterWrapper> <DeckyGamepadRouterWrapper>{ret}</DeckyGamepadRouterWrapper>
</DeckyRouterStateContextProvider> </DeckyRouterStateContextProvider>
<DeckyGlobalComponentsStateContextProvider deckyGlobalComponentsState={this.globalComponentsState}> <DeckyGlobalComponentsStateContextProvider deckyGlobalComponentsState={this.globalComponentsState}>
<DeckyGlobalComponentsWrapper /> <DeckyGlobalComponentsWrapper uiMode={UIMode.BigPicture} />
</DeckyGlobalComponentsStateContextProvider> </DeckyGlobalComponentsStateContextProvider>
</> </>
); );
@@ -227,13 +238,21 @@ class RouterHook extends Logger {
return returnVal; return returnVal;
} }
private globalComponentsWrapper() { private globalComponentsWrapper({ uiMode }: { uiMode: UIMode }) {
const { components } = useDeckyGlobalComponentsState(); const { components } = useDeckyGlobalComponentsState();
if (this.renderedComponents.length != components.size) { const componentsForMode = components.get(uiMode);
this.debug('Rerendering global components'); if (!componentsForMode) {
this.renderedComponents = Array.from(components.values()).map((GComponent) => <GComponent />); this.warn(`Couldn't find global components map for uimode ${uiMode}`);
return null;
} }
return <>{this.renderedComponents}</>; if (!this.renderedComponents.has(uiMode) || this.renderedComponents.get(uiMode)?.length != componentsForMode.size) {
this.debug('Rerendering global components for uiMode', uiMode);
this.renderedComponents.set(
uiMode,
Array.from(componentsForMode.values()).map((GComponent) => <GComponent />),
);
}
return <>{this.renderedComponents.get(uiMode)}</>;
} }
private gamepadRouterWrapper({ children }: { children: ReactElement }) { private gamepadRouterWrapper({ children }: { children: ReactElement }) {
@@ -247,8 +266,8 @@ class RouterHook extends Logger {
} }
const mainRouteList = children.props.children[0].props.children; const mainRouteList = children.props.children[0].props.children;
const ingameRouteList = children.props.children[1].props.children; // /appoverlay and /apprunning const ingameRouteList = children.props.children[1].props.children; // /appoverlay and /apprunning
this.processList(mainRouteList, routes, routePatches, true); this.processList(mainRouteList, routes, routePatches.get(UIMode.BigPicture), true, this.Route);
this.processList(ingameRouteList, null, routePatches, false); this.processList(ingameRouteList, null, routePatches.get(UIMode.BigPicture), false, this.Route);
this.debug('Rerendered gamepadui routes list'); this.debug('Rerendered gamepadui routes list');
return children; return children;
@@ -256,22 +275,38 @@ class RouterHook extends Logger {
private desktopRouterWrapper({ children }: { children: ReactElement }) { private desktopRouterWrapper({ children }: { children: ReactElement }) {
// Used to store the new replicated routes we create to allow routes to be unpatched. // Used to store the new replicated routes we create to allow routes to be unpatched.
this.debug('desktop router wrapper render', children);
const { routes, routePatches } = useDeckyRouterState(); const { routes, routePatches } = useDeckyRouterState();
const routeList = findInReactTree( const mainRouteList = findInReactTree(
children, children,
(node) => node?.length > 2 && node?.find((elem: any) => elem?.props?.path == '/library/home'), (node) => node?.length > 2 && node?.find((elem: any) => elem?.props?.path == '/console'),
); );
if (!routeList) { if (!mainRouteList) {
this.debug('routerWrapper wrong component?', children); this.debug('routerWrapper wrong component?', children);
return children; return children;
} }
const library = children.props.children[1].props.children.props; this.processList(mainRouteList, routes, routePatches.get(UIMode.Desktop), true, this.DesktopRoute);
if (!Array.isArray(library.children)) { const libraryRouteWrapper = mainRouteList.find(
library.children = [library.children]; (r: any) => r?.props && 'cm' in r.props && 'bShowDesktopUIContent' in r.props,
);
if (!this.wrappedDesktopLibraryMemo) {
wrapReactType(libraryRouteWrapper);
afterPatch(libraryRouteWrapper.type, 'type', (_, ret) => {
const { routePatches } = useDeckyRouterState();
const libraryRouteList = findInReactTree(
ret,
(node) => node?.length > 1 && node?.find((elem: any) => elem?.props?.path == '/library/downloads'),
);
if (!libraryRouteList) {
this.warn('failed to find library route list', ret);
return ret;
}
this.processList(libraryRouteList, null, routePatches.get(UIMode.Desktop), false, this.DesktopRoute);
return ret;
});
this.wrappedDesktopLibraryMemo = libraryRouteWrapper.type;
} else {
libraryRouteWrapper.type = this.wrappedDesktopLibraryMemo;
} }
this.debug('library', library);
this.processList(library.children, routes, routePatches, true);
this.debug('Rerendered desktop routes list'); this.debug('Rerendered desktop routes list');
return children; return children;
@@ -279,11 +314,11 @@ class RouterHook extends Logger {
private processList( private processList(
routeList: any[], routeList: any[],
routes: Map<string, RouterEntry> | null, routes: Map<string, RouterEntry> | null | undefined,
routePatches: Map<string, Set<RoutePatch>>, routePatches: Map<string, Set<RoutePatch>> | null | undefined,
save: boolean, save: boolean,
RouteComponent: any,
) { ) {
const Route = this.Route;
this.debug('Route list: ', routeList); this.debug('Route list: ', routeList);
if (save) this.routes = routeList; if (save) this.routes = routeList;
let routerIndex = routeList.length; let routerIndex = routeList.length;
@@ -293,59 +328,60 @@ class RouterHook extends Logger {
const newRouterArray: (ReactElement | JSX.Element)[] = []; const newRouterArray: (ReactElement | JSX.Element)[] = [];
routes.forEach(({ component, props }, path) => { routes.forEach(({ component, props }, path) => {
newRouterArray.push( newRouterArray.push(
<Route path={path} {...props}> <RouteComponent path={path} {...props}>
<ErrorBoundary>{createElement(component)}</ErrorBoundary> <ErrorBoundary>{createElement(component)}</ErrorBoundary>
</Route>, </RouteComponent>,
); );
}); });
routeList[routerIndex] = newRouterArray; routeList[routerIndex] = newRouterArray;
} }
} }
routeList.forEach((route: Route, index: number) => { routePatches &&
const replaced = this.toReplace.get(route?.props?.path as string); routeList.forEach((route: Route, index: number) => {
if (replaced) { const replaced = this.toReplace.get(route?.props?.path as string);
routeList[index].props.children = replaced; if (replaced) {
this.toReplace.delete(route?.props?.path as string); routeList[index].props.children = replaced;
} this.toReplace.delete(route?.props?.path as string);
if (route?.props?.path && routePatches.has(route.props.path as string)) { }
this.toReplace.set( if (route?.props?.path && routePatches.has(route.props.path as string)) {
route?.props?.path as string, this.toReplace.set(
// @ts-ignore route?.props?.path as string,
routeList[index].props.children, // @ts-ignore
); routeList[index].props.children,
routePatches.get(route.props.path as string)?.forEach((patch) => { );
const oType = routeList[index].props.children.type; routePatches.get(route.props.path as string)?.forEach((patch) => {
routeList[index].props.children = patch({ const oType = routeList[index].props.children.type;
...routeList[index].props, routeList[index].props.children = patch({
children: { ...routeList[index].props,
...cloneElement(routeList[index].props.children), children: {
type: routeList[index].props.children[isPatched] ? oType : (props) => createElement(oType, props), ...cloneElement(routeList[index].props.children),
}, type: routeList[index].props.children[isPatched] ? oType : (props) => createElement(oType, props),
}).children; },
routeList[index].props.children[isPatched] = true; }).children;
}); routeList[index].props.children[isPatched] = true;
} });
}); }
});
} }
addRoute(path: string, component: RouterEntry['component'], props: RouterEntry['props'] = {}) { addRoute(path: string, component: RouterEntry['component'], props: RouterEntry['props'] = {}) {
this.routerState.addRoute(path, component, props); this.routerState.addRoute(path, component, props);
} }
addPatch(path: string, patch: RoutePatch) { addPatch(path: string, patch: RoutePatch, uiMode: UIMode = UIMode.BigPicture) {
return this.routerState.addPatch(path, patch); return this.routerState.addPatch(path, patch, uiMode);
} }
addGlobalComponent(name: string, component: FC) { addGlobalComponent(name: string, component: FC, uiMode: UIMode = UIMode.BigPicture) {
this.globalComponentsState.addComponent(name, component); this.globalComponentsState.addComponent(name, component, uiMode);
} }
removeGlobalComponent(name: string) { removeGlobalComponent(name: string, uiMode: UIMode = UIMode.BigPicture) {
this.globalComponentsState.removeComponent(name); this.globalComponentsState.removeComponent(name, uiMode);
} }
removePatch(path: string, patch: RoutePatch) { removePatch(path: string, patch: RoutePatch, uiMode: UIMode = UIMode.BigPicture) {
this.routerState.removePatch(path, patch); this.routerState.removePatch(path, patch, uiMode);
} }
removeRoute(path: string) { removeRoute(path: string) {