Skip to content

Commit

Permalink
Add routerhook for desktop UI and a basic sidebar menu for Decky in d…
Browse files Browse the repository at this point in the history
…esktop UI
  • Loading branch information
AAGaming00 committed Oct 5, 2024
1 parent fc95cf5 commit 7e040c1
Show file tree
Hide file tree
Showing 18 changed files with 448 additions and 181 deletions.
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
}
},
"dependencies": {
"@decky/ui": "^4.7.4",
"@decky/ui": "^4.8.0",
"compare-versions": "^6.1.1",
"filesize": "^10.1.2",
"i18next": "^23.11.5",
Expand Down
18 changes: 9 additions & 9 deletions frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

72 changes: 72 additions & 0 deletions frontend/src/components/DeckyDesktopSidebar.tsx
Original file line number Diff line number Diff line change
@@ -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;
44 changes: 44 additions & 0 deletions frontend/src/components/DeckyDesktopUI.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { CSSProperties, FC, useState } 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;
27 changes: 19 additions & 8 deletions frontend/src/components/DeckyGlobalComponentsState.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';

import { UIMode } from '../enums';

interface PublicDeckyGlobalComponentsState {
components: Map<string, FC>;
components: Map<UIMode, Map<string, FC>>;
}

export class DeckyGlobalComponentsState {
// 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();

publicState(): PublicDeckyGlobalComponentsState {
return { components: this._components };
}

addComponent(path: string, component: FC) {
this._components.set(path, component);
addComponent(path: string, component: FC, uiMode: UIMode) {
const components = this._components.get(uiMode);
if (!components) throw new Error(`UI mode ${uiMode} not supported.`);

components.set(path, component);
this.notifyUpdate();
}

removeComponent(path: string) {
this._components.delete(path);
removeComponent(path: string, uiMode: UIMode) {
const components = this._components.get(uiMode);
if (!components) throw new Error(`UI mode ${uiMode} not supported.`);

components.delete(path);
this.notifyUpdate();
}

Expand All @@ -30,8 +41,8 @@ export class DeckyGlobalComponentsState {
}

interface DeckyGlobalComponentsContext extends PublicDeckyGlobalComponentsState {
addComponent(path: string, component: FC): void;
removeComponent(path: string): void;
addComponent(path: string, component: FC, uiMode: UIMode): void;
removeComponent(path: string, uiMode: UIMode): void;
}

const DeckyGlobalComponentsContext = createContext<DeckyGlobalComponentsContext>(null as any);
Expand Down
30 changes: 20 additions & 10 deletions frontend/src/components/DeckyRouterState.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ComponentType, FC, ReactNode, createContext, useContext, useEffect, useState } from 'react';
import type { RouteProps } from 'react-router';

import { UIMode } from '../enums';

export interface RouterEntry {
props: Omit<RouteProps, 'path' | 'children'>;
component: ComponentType;
Expand All @@ -10,12 +12,16 @@ export type RoutePatch = (route: RouteProps) => RouteProps;

interface PublicDeckyRouterState {
routes: Map<string, RouterEntry>;
routePatches: Map<string, Set<RoutePatch>>;
routePatches: Map<UIMode, Map<string, Set<RoutePatch>>>;
}

export class DeckyRouterState {
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();

Expand All @@ -28,22 +34,26 @@ export class DeckyRouterState {
this.notifyUpdate();
}

addPatch(path: string, patch: RoutePatch) {
let patchList = this._routePatches.get(path);
addPatch(path: string, patch: RoutePatch, uiMode: UIMode) {
const patchesForMode = this._routePatches.get(uiMode);
if (!patchesForMode) throw new Error(`UI mode ${uiMode} not supported.`);
let patchList = patchesForMode.get(path);
if (!patchList) {
patchList = new Set();
this._routePatches.set(path, patchList);
patchesForMode.set(path, patchList);
}
patchList.add(patch);
this.notifyUpdate();
return patch;
}

removePatch(path: string, patch: RoutePatch) {
const patchList = this._routePatches.get(path);
removePatch(path: string, patch: RoutePatch, uiMode: UIMode) {
const patchesForMode = this._routePatches.get(uiMode);
if (!patchesForMode) throw new Error(`UI mode ${uiMode} not supported.`);
const patchList = patchesForMode.get(path);
patchList?.delete(patch);
if (patchList?.size == 0) {
this._routePatches.delete(path);
patchesForMode.delete(path);
}
this.notifyUpdate();
}
Expand All @@ -60,8 +70,8 @@ export class DeckyRouterState {

interface DeckyRouterStateContext extends PublicDeckyRouterState {
addRoute(path: string, component: RouterEntry['component'], props: RouterEntry['props']): void;
addPatch(path: string, patch: RoutePatch): RoutePatch;
removePatch(path: string, patch: RoutePatch): void;
addPatch(path: string, patch: RoutePatch, uiMode?: UIMode): RoutePatch;
removePatch(path: string, patch: RoutePatch, uiMode?: UIMode): void;
removeRoute(path: string): void;
}

Expand Down
11 changes: 11 additions & 0 deletions frontend/src/components/DeckyState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface PublicDeckyState {
versionInfo: VerInfo | null;
notificationSettings: NotificationSettings;
userInfo: UserInfo | null;
desktopMenuOpen: boolean;
}

export interface UserInfo {
Expand All @@ -36,6 +37,7 @@ export class DeckyState {
private _versionInfo: VerInfo | null = null;
private _notificationSettings = DEFAULT_NOTIFICATION_SETTINGS;
private _userInfo: UserInfo | null = null;
private _desktopMenuOpen: boolean = false;

public eventBus = new EventTarget();

Expand All @@ -52,6 +54,7 @@ export class DeckyState {
versionInfo: this._versionInfo,
notificationSettings: this._notificationSettings,
userInfo: this._userInfo,
desktopMenuOpen: this._desktopMenuOpen,
};
}

Expand Down Expand Up @@ -115,6 +118,11 @@ export class DeckyState {
this.notifyUpdate();
}

setDesktopMenuOpen(open: boolean) {
this._desktopMenuOpen = open;
this.notifyUpdate();
}

private notifyUpdate() {
this.eventBus.dispatchEvent(new Event('update'));
}
Expand All @@ -126,6 +134,7 @@ interface DeckyStateContext extends PublicDeckyState {
setActivePlugin(name: string): void;
setPluginOrder(pluginOrder: string[]): void;
closeActivePlugin(): void;
setDesktopMenuOpen(open: boolean): void;
}

const DeckyStateContext = createContext<DeckyStateContext>(null as any);
Expand Down Expand Up @@ -155,6 +164,7 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
const setActivePlugin = deckyState.setActivePlugin.bind(deckyState);
const closeActivePlugin = deckyState.closeActivePlugin.bind(deckyState);
const setPluginOrder = deckyState.setPluginOrder.bind(deckyState);
const setDesktopMenuOpen = deckyState.setDesktopMenuOpen.bind(deckyState);

return (
<DeckyStateContext.Provider
Expand All @@ -165,6 +175,7 @@ export const DeckyStateContextProvider: FC<Props> = ({ children, deckyState }) =
setActivePlugin,
closeActivePlugin,
setPluginOrder,
setDesktopMenuOpen,
}}
>
{children}
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/components/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ const Markdown: FunctionComponent<MarkdownProps> = (props) => {
props.onDismiss?.();
Navigation.NavigateToExternalWeb(aRef.current!.href);
}}
onClick={(e) => {
e.preventDefault();
props.onDismiss?.();
Navigation.NavigateToExternalWeb(aRef.current!.href);
}}
style={{ display: 'inline' }}
>
<a ref={aRef} {...nodeProps.node.properties}>
Expand Down
Loading

0 comments on commit 7e040c1

Please sign in to comment.