mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-07-12 22:11:56 +00:00
Add routerhook for desktop UI and a basic sidebar menu for Decky in desktop UI
This commit is contained in:
Generated
+4
-4
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export enum UIMode {
|
||||||
|
BigPicture = 4,
|
||||||
|
Desktop = 7,
|
||||||
|
}
|
||||||
@@ -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
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user