Skip to content

Commit 7e040c1

Browse files
committed
Add routerhook for desktop UI and a basic sidebar menu for Decky in desktop UI
1 parent fc95cf5 commit 7e040c1

18 files changed

+448
-181
lines changed

frontend/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
}
5050
},
5151
"dependencies": {
52-
"@decky/ui": "^4.7.4",
52+
"@decky/ui": "^4.8.0",
5353
"compare-versions": "^6.1.1",
5454
"filesize": "^10.1.2",
5555
"i18next": "^23.11.5",

frontend/pnpm-lock.yaml

+9-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { FC, useEffect, useRef, useState } from 'react';
2+
3+
import { useDeckyState } from './DeckyState';
4+
import PluginView from './PluginView';
5+
import { QuickAccessVisibleState } from './QuickAccessVisibleState';
6+
7+
const DeckyDesktopSidebar: FC = () => {
8+
const { desktopMenuOpen, setDesktopMenuOpen } = useDeckyState();
9+
const [closed, setClosed] = useState<boolean>(!desktopMenuOpen);
10+
const [openAnimStart, setOpenAnimStart] = useState<boolean>(desktopMenuOpen);
11+
const closedInterval = useRef<number | null>(null);
12+
13+
useEffect(() => {
14+
const anim = requestAnimationFrame(() => setOpenAnimStart(desktopMenuOpen));
15+
return () => cancelAnimationFrame(anim);
16+
}, [desktopMenuOpen]);
17+
18+
useEffect(() => {
19+
closedInterval.current && clearTimeout(closedInterval.current);
20+
if (desktopMenuOpen) {
21+
setClosed(false);
22+
} else {
23+
closedInterval.current = setTimeout(() => setClosed(true), 500);
24+
}
25+
}, [desktopMenuOpen]);
26+
return (
27+
<>
28+
<div
29+
className="deckyDesktopSidebarDim"
30+
style={{
31+
position: 'absolute',
32+
height: 'calc(100% - 78px - 50px)',
33+
width: '100%',
34+
top: '78px',
35+
left: '0px',
36+
zIndex: 998,
37+
background: 'rgba(0, 0, 0, 0.7)',
38+
opacity: openAnimStart ? 1 : 0,
39+
display: desktopMenuOpen || !closed ? 'flex' : 'none',
40+
transition: 'opacity 0.4s cubic-bezier(0.65, 0, 0.35, 1)',
41+
}}
42+
onClick={() => setDesktopMenuOpen(false)}
43+
/>
44+
45+
<div
46+
className="deckyDesktopSidebar"
47+
style={{
48+
position: 'absolute',
49+
height: 'calc(100% - 78px - 50px)',
50+
width: '350px',
51+
paddingLeft: '16px',
52+
top: '78px',
53+
right: '0px',
54+
zIndex: 999,
55+
transition: 'transform 0.4s cubic-bezier(0.65, 0, 0.35, 1)',
56+
transform: openAnimStart ? 'translateX(0px)' : 'translateX(366px)',
57+
overflowY: 'scroll',
58+
// prevents chromium border jank
59+
display: desktopMenuOpen || !closed ? 'flex' : 'none',
60+
flexDirection: 'column',
61+
background: '#171d25',
62+
}}
63+
>
64+
<QuickAccessVisibleState.Provider value={desktopMenuOpen || !closed}>
65+
<PluginView desktop={true} />
66+
</QuickAccessVisibleState.Provider>
67+
</div>
68+
</>
69+
);
70+
};
71+
72+
export default DeckyDesktopSidebar;
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { CSSProperties, FC, useState } from 'react';
2+
3+
import DeckyDesktopSidebar from './DeckyDesktopSidebar';
4+
import DeckyIcon from './DeckyIcon';
5+
import { useDeckyState } from './DeckyState';
6+
7+
const DeckyDesktopUI: FC = () => {
8+
const { desktopMenuOpen, setDesktopMenuOpen } = useDeckyState();
9+
return (
10+
<>
11+
<style>
12+
{`
13+
.deckyDesktopIcon {
14+
color: #67707b;
15+
}
16+
.deckyDesktopIcon:hover {
17+
color: #fff;
18+
}
19+
`}
20+
</style>
21+
<DeckyIcon
22+
className="deckyDesktopIcon"
23+
width={24}
24+
height={24}
25+
onClick={() => setDesktopMenuOpen(!desktopMenuOpen)}
26+
style={
27+
{
28+
position: 'absolute',
29+
top: '36px', // nav text is 34px but 36px looks nicer to me
30+
right: '10px', // <- is 16px but 10px looks nicer to me
31+
width: '24px',
32+
height: '24px',
33+
cursor: 'pointer',
34+
transition: 'color 0.3s linear',
35+
'-webkit-app-region': 'no-drag',
36+
} as CSSProperties
37+
}
38+
/>
39+
<DeckyDesktopSidebar />
40+
</>
41+
);
42+
};
43+
44+
export default DeckyDesktopUI;

frontend/src/components/DeckyGlobalComponentsState.tsx

+19-8
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,37 @@
11
import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
22

3+
import { UIMode } from '../enums';
4+
35
interface PublicDeckyGlobalComponentsState {
4-
components: Map<string, FC>;
6+
components: Map<UIMode, Map<string, FC>>;
57
}
68

79
export class DeckyGlobalComponentsState {
810
// TODO a set would be better
9-
private _components = new Map<string, FC>();
11+
private _components = new Map<UIMode, Map<string, FC>>([
12+
[UIMode.BigPicture, new Map()],
13+
[UIMode.Desktop, new Map()],
14+
]);
1015

1116
public eventBus = new EventTarget();
1217

1318
publicState(): PublicDeckyGlobalComponentsState {
1419
return { components: this._components };
1520
}
1621

17-
addComponent(path: string, component: FC) {
18-
this._components.set(path, component);
22+
addComponent(path: string, component: FC, uiMode: UIMode) {
23+
const components = this._components.get(uiMode);
24+
if (!components) throw new Error(`UI mode ${uiMode} not supported.`);
25+
26+
components.set(path, component);
1927
this.notifyUpdate();
2028
}
2129

22-
removeComponent(path: string) {
23-
this._components.delete(path);
30+
removeComponent(path: string, uiMode: UIMode) {
31+
const components = this._components.get(uiMode);
32+
if (!components) throw new Error(`UI mode ${uiMode} not supported.`);
33+
34+
components.delete(path);
2435
this.notifyUpdate();
2536
}
2637

@@ -30,8 +41,8 @@ export class DeckyGlobalComponentsState {
3041
}
3142

3243
interface DeckyGlobalComponentsContext extends PublicDeckyGlobalComponentsState {
33-
addComponent(path: string, component: FC): void;
34-
removeComponent(path: string): void;
44+
addComponent(path: string, component: FC, uiMode: UIMode): void;
45+
removeComponent(path: string, uiMode: UIMode): void;
3546
}
3647

3748
const DeckyGlobalComponentsContext = createContext<DeckyGlobalComponentsContext>(null as any);

frontend/src/components/DeckyRouterState.tsx

+20-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { ComponentType, FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
22
import type { RouteProps } from 'react-router';
33

4+
import { UIMode } from '../enums';
5+
46
export interface RouterEntry {
57
props: Omit<RouteProps, 'path' | 'children'>;
68
component: ComponentType;
@@ -10,12 +12,16 @@ export type RoutePatch = (route: RouteProps) => RouteProps;
1012

1113
interface PublicDeckyRouterState {
1214
routes: Map<string, RouterEntry>;
13-
routePatches: Map<string, Set<RoutePatch>>;
15+
routePatches: Map<UIMode, Map<string, Set<RoutePatch>>>;
1416
}
1517

1618
export class DeckyRouterState {
1719
private _routes = new Map<string, RouterEntry>();
18-
private _routePatches = new Map<string, Set<RoutePatch>>();
20+
// Update when support for new UIModes is added
21+
private _routePatches = new Map<UIMode, Map<string, Set<RoutePatch>>>([
22+
[UIMode.BigPicture, new Map()],
23+
[UIMode.Desktop, new Map()],
24+
]);
1925

2026
public eventBus = new EventTarget();
2127

@@ -28,22 +34,26 @@ export class DeckyRouterState {
2834
this.notifyUpdate();
2935
}
3036

31-
addPatch(path: string, patch: RoutePatch) {
32-
let patchList = this._routePatches.get(path);
37+
addPatch(path: string, patch: RoutePatch, uiMode: UIMode) {
38+
const patchesForMode = this._routePatches.get(uiMode);
39+
if (!patchesForMode) throw new Error(`UI mode ${uiMode} not supported.`);
40+
let patchList = patchesForMode.get(path);
3341
if (!patchList) {
3442
patchList = new Set();
35-
this._routePatches.set(path, patchList);
43+
patchesForMode.set(path, patchList);
3644
}
3745
patchList.add(patch);
3846
this.notifyUpdate();
3947
return patch;
4048
}
4149

42-
removePatch(path: string, patch: RoutePatch) {
43-
const patchList = this._routePatches.get(path);
50+
removePatch(path: string, patch: RoutePatch, uiMode: UIMode) {
51+
const patchesForMode = this._routePatches.get(uiMode);
52+
if (!patchesForMode) throw new Error(`UI mode ${uiMode} not supported.`);
53+
const patchList = patchesForMode.get(path);
4454
patchList?.delete(patch);
4555
if (patchList?.size == 0) {
46-
this._routePatches.delete(path);
56+
patchesForMode.delete(path);
4757
}
4858
this.notifyUpdate();
4959
}
@@ -60,8 +70,8 @@ export class DeckyRouterState {
6070

6171
interface DeckyRouterStateContext extends PublicDeckyRouterState {
6272
addRoute(path: string, component: RouterEntry['component'], props: RouterEntry['props']): void;
63-
addPatch(path: string, patch: RoutePatch): RoutePatch;
64-
removePatch(path: string, patch: RoutePatch): void;
73+
addPatch(path: string, patch: RoutePatch, uiMode?: UIMode): RoutePatch;
74+
removePatch(path: string, patch: RoutePatch, uiMode?: UIMode): void;
6575
removeRoute(path: string): void;
6676
}
6777

frontend/src/components/DeckyState.tsx

+11
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ interface PublicDeckyState {
1717
versionInfo: VerInfo | null;
1818
notificationSettings: NotificationSettings;
1919
userInfo: UserInfo | null;
20+
desktopMenuOpen: boolean;
2021
}
2122

2223
export interface UserInfo {
@@ -36,6 +37,7 @@ export class DeckyState {
3637
private _versionInfo: VerInfo | null = null;
3738
private _notificationSettings = DEFAULT_NOTIFICATION_SETTINGS;
3839
private _userInfo: UserInfo | null = null;
40+
private _desktopMenuOpen: boolean = false;
3941

4042
public eventBus = new EventTarget();
4143

@@ -52,6 +54,7 @@ export class DeckyState {
5254
versionInfo: this._versionInfo,
5355
notificationSettings: this._notificationSettings,
5456
userInfo: this._userInfo,
57+
desktopMenuOpen: this._desktopMenuOpen,
5558
};
5659
}
5760

@@ -115,6 +118,11 @@ export class DeckyState {
115118
this.notifyUpdate();
116119
}
117120

121+
setDesktopMenuOpen(open: boolean) {
122+
this._desktopMenuOpen = open;
123+
this.notifyUpdate();
124+
}
125+
118126
private notifyUpdate() {
119127
this.eventBus.dispatchEvent(new Event('update'));
120128
}
@@ -126,6 +134,7 @@ interface DeckyStateContext extends PublicDeckyState {
126134
setActivePlugin(name: string): void;
127135
setPluginOrder(pluginOrder: string[]): void;
128136
closeActivePlugin(): void;
137+
setDesktopMenuOpen(open: boolean): void;
129138
}
130139

131140
const DeckyStateContext = createContext<DeckyStateContext>(null as any);
@@ -155,6 +164,7 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
155164
const setActivePlugin = deckyState.setActivePlugin.bind(deckyState);
156165
const closeActivePlugin = deckyState.closeActivePlugin.bind(deckyState);
157166
const setPluginOrder = deckyState.setPluginOrder.bind(deckyState);
167+
const setDesktopMenuOpen = deckyState.setDesktopMenuOpen.bind(deckyState);
158168

159169
return (
160170
<DeckyStateContext.Provider
@@ -165,6 +175,7 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
165175
setActivePlugin,
166176
closeActivePlugin,
167177
setPluginOrder,
178+
setDesktopMenuOpen,
168179
}}
169180
>
170181
{children}

frontend/src/components/Markdown.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ const Markdown: FunctionComponent<MarkdownProps> = (props) => {
2424
props.onDismiss?.();
2525
Navigation.NavigateToExternalWeb(aRef.current!.href);
2626
}}
27+
onClick={(e) => {
28+
e.preventDefault();
29+
props.onDismiss?.();
30+
Navigation.NavigateToExternalWeb(aRef.current!.href);
31+
}}
2732
style={{ display: 'inline' }}
2833
>
2934
<a ref={aRef} {...nodeProps.node.properties}>

0 commit comments

Comments
 (0)