From 7b32df09487383897927356547f1ba5a73e8cc94 Mon Sep 17 00:00:00 2001 From: AAGaming Date: Fri, 4 Oct 2024 23:59:53 -0400 Subject: Add routerhook for desktop UI and a basic sidebar menu for Decky in desktop UI --- frontend/src/components/DeckyDesktopSidebar.tsx | 72 ++++++++++++++++++ frontend/src/components/DeckyDesktopUI.tsx | 44 +++++++++++ .../src/components/DeckyGlobalComponentsState.tsx | 27 +++++-- frontend/src/components/DeckyRouterState.tsx | 30 +++++--- frontend/src/components/DeckyState.tsx | 11 +++ frontend/src/components/Markdown.tsx | 5 ++ frontend/src/components/PluginView.tsx | 10 ++- .../src/components/QuickAccessVisibleState.tsx | 2 +- frontend/src/components/TitleView.tsx | 29 +++++-- .../modals/MultiplePluginsInstallModal.tsx | 5 +- .../src/components/modals/PluginInstallModal.tsx | 5 +- frontend/src/components/settings/index.tsx | 17 ++++- .../components/settings/pages/general/Updater.tsx | 88 ++++++++++++---------- 13 files changed, 271 insertions(+), 74 deletions(-) create mode 100644 frontend/src/components/DeckyDesktopSidebar.tsx create mode 100644 frontend/src/components/DeckyDesktopUI.tsx (limited to 'frontend/src/components') diff --git a/frontend/src/components/DeckyDesktopSidebar.tsx b/frontend/src/components/DeckyDesktopSidebar.tsx new file mode 100644 index 00000000..f159652b --- /dev/null +++ b/frontend/src/components/DeckyDesktopSidebar.tsx @@ -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(!desktopMenuOpen); + const [openAnimStart, setOpenAnimStart] = useState(desktopMenuOpen); + const closedInterval = useRef(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 ( + <> +
setDesktopMenuOpen(false)} + /> + +
+ + + +
+ + ); +}; + +export default DeckyDesktopSidebar; diff --git a/frontend/src/components/DeckyDesktopUI.tsx b/frontend/src/components/DeckyDesktopUI.tsx new file mode 100644 index 00000000..fde33c0f --- /dev/null +++ b/frontend/src/components/DeckyDesktopUI.tsx @@ -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 ( + <> + + 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 + } + /> + + + ); +}; + +export default DeckyDesktopUI; diff --git a/frontend/src/components/DeckyGlobalComponentsState.tsx b/frontend/src/components/DeckyGlobalComponentsState.tsx index 475d1e4a..4088f7b1 100644 --- a/frontend/src/components/DeckyGlobalComponentsState.tsx +++ b/frontend/src/components/DeckyGlobalComponentsState.tsx @@ -1,12 +1,17 @@ import { FC, ReactNode, createContext, useContext, useEffect, useState } from 'react'; +import { UIMode } from '../enums'; + interface PublicDeckyGlobalComponentsState { - components: Map; + components: Map>; } export class DeckyGlobalComponentsState { // TODO a set would be better - private _components = new Map(); + private _components = new Map>([ + [UIMode.BigPicture, new Map()], + [UIMode.Desktop, new Map()], + ]); public eventBus = new EventTarget(); @@ -14,13 +19,19 @@ export class DeckyGlobalComponentsState { 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(); } @@ -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(null as any); diff --git a/frontend/src/components/DeckyRouterState.tsx b/frontend/src/components/DeckyRouterState.tsx index 426ed731..f13855a4 100644 --- a/frontend/src/components/DeckyRouterState.tsx +++ b/frontend/src/components/DeckyRouterState.tsx @@ -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; component: ComponentType; @@ -10,12 +12,16 @@ export type RoutePatch = (route: RouteProps) => RouteProps; interface PublicDeckyRouterState { routes: Map; - routePatches: Map>; + routePatches: Map>>; } export class DeckyRouterState { private _routes = new Map(); - private _routePatches = new Map>(); + // Update when support for new UIModes is added + private _routePatches = new Map>>([ + [UIMode.BigPicture, new Map()], + [UIMode.Desktop, new Map()], + ]); public eventBus = new EventTarget(); @@ -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(); } @@ -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; } diff --git a/frontend/src/components/DeckyState.tsx b/frontend/src/components/DeckyState.tsx index 75106e62..ddd8e052 100644 --- a/frontend/src/components/DeckyState.tsx +++ b/frontend/src/components/DeckyState.tsx @@ -17,6 +17,7 @@ interface PublicDeckyState { versionInfo: VerInfo | null; notificationSettings: NotificationSettings; userInfo: UserInfo | null; + desktopMenuOpen: boolean; } export interface UserInfo { @@ -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(); @@ -52,6 +54,7 @@ export class DeckyState { versionInfo: this._versionInfo, notificationSettings: this._notificationSettings, userInfo: this._userInfo, + desktopMenuOpen: this._desktopMenuOpen, }; } @@ -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')); } @@ -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(null as any); @@ -155,6 +164,7 @@ export const DeckyStateContextProvider: FC = ({ 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 ( = ({ children, deckyState }) = setActivePlugin, closeActivePlugin, setPluginOrder, + setDesktopMenuOpen, }} > {children} diff --git a/frontend/src/components/Markdown.tsx b/frontend/src/components/Markdown.tsx index cf6657aa..d6201980 100644 --- a/frontend/src/components/Markdown.tsx +++ b/frontend/src/components/Markdown.tsx @@ -24,6 +24,11 @@ const Markdown: FunctionComponent = (props) => { props.onDismiss?.(); Navigation.NavigateToExternalWeb(aRef.current!.href); }} + onClick={(e) => { + e.preventDefault(); + props.onDismiss?.(); + Navigation.NavigateToExternalWeb(aRef.current!.href); + }} style={{ display: 'inline' }} > diff --git a/frontend/src/components/PluginView.tsx b/frontend/src/components/PluginView.tsx index 19afbca5..e36df3cb 100644 --- a/frontend/src/components/PluginView.tsx +++ b/frontend/src/components/PluginView.tsx @@ -9,7 +9,11 @@ import NotificationBadge from './NotificationBadge'; import { useQuickAccessVisible } from './QuickAccessVisibleState'; import TitleView from './TitleView'; -const PluginView: FC = () => { +interface PluginViewProps { + desktop?: boolean; +} + +const PluginView: FC = ({ desktop = false }) => { const { hiddenPlugins } = useDeckyState(); const { plugins, updates, activePlugin, pluginOrder, setActivePlugin, closeActivePlugin } = useDeckyState(); const visible = useQuickAccessVisible(); @@ -27,7 +31,7 @@ const PluginView: FC = () => { if (activePlugin) { return ( - +
{(visible || activePlugin.alwaysRender) && activePlugin.content}
@@ -36,7 +40,7 @@ const PluginView: FC = () => { } return ( <> - +
(false); +export const QuickAccessVisibleState = createContext(false); export const useQuickAccessVisible = () => useContext(QuickAccessVisibleState); diff --git a/frontend/src/components/TitleView.tsx b/frontend/src/components/TitleView.tsx index 0cb82b7f..8ddb242d 100644 --- a/frontend/src/components/TitleView.tsx +++ b/frontend/src/components/TitleView.tsx @@ -14,18 +14,34 @@ const titleStyles: CSSProperties = { top: '0px', }; -const TitleView: FC = () => { - const { activePlugin, closeActivePlugin } = useDeckyState(); +interface TitleViewProps { + desktop?: boolean; +} + +const TitleView: FC = ({ desktop }) => { + const { activePlugin, closeActivePlugin, setDesktopMenuOpen } = useDeckyState(); const { t } = useTranslation(); const onSettingsClick = () => { Navigation.Navigate('/decky/settings'); Navigation.CloseSideMenus(); + setDesktopMenuOpen(false); }; const onStoreClick = () => { Navigation.Navigate('/decky/store'); 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) { @@ -33,14 +49,14 @@ const TitleView: FC = () => {
Decky
@@ -52,10 +68,7 @@ const TitleView: FC = () => { return ( - + {activePlugin?.titleView ||
{activePlugin.name}
} diff --git a/frontend/src/components/modals/MultiplePluginsInstallModal.tsx b/frontend/src/components/modals/MultiplePluginsInstallModal.tsx index ba49ba92..d6f163f7 100644 --- a/frontend/src/components/modals/MultiplePluginsInstallModal.tsx +++ b/frontend/src/components/modals/MultiplePluginsInstallModal.tsx @@ -80,7 +80,10 @@ const MultiplePluginsInstallModal: FC = ({ onOK={async () => { setLoading(true); await onOK(); - setTimeout(() => Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky), 250); + setTimeout(() => { + Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky); + DeckyPluginLoader.setDesktopMenuOpen(true); + }, 250); setTimeout(() => DeckyPluginLoader.checkPluginUpdates(), 1000); }} onCancel={async () => { diff --git a/frontend/src/components/modals/PluginInstallModal.tsx b/frontend/src/components/modals/PluginInstallModal.tsx index 227bd818..ec353279 100644 --- a/frontend/src/components/modals/PluginInstallModal.tsx +++ b/frontend/src/components/modals/PluginInstallModal.tsx @@ -51,7 +51,10 @@ const PluginInstallModal: FC = ({ onOK={async () => { setLoading(true); await onOK(); - setTimeout(() => Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky), 250); + setTimeout(() => { + Navigation.OpenQuickAccessMenu(QuickAccessTab.Decky); + DeckyPluginLoader.setDesktopMenuOpen(true); + }, 250); setTimeout(() => DeckyPluginLoader.checkPluginUpdates(), 1000); }} onCancel={async () => { diff --git a/frontend/src/components/settings/index.tsx b/frontend/src/components/settings/index.tsx index d6d98645..cb5096f5 100644 --- a/frontend/src/components/settings/index.tsx +++ b/frontend/src/components/settings/index.tsx @@ -53,5 +53,20 @@ export default function SettingsPage() { }, ]; - return ; + return ( +
+ + +
+ ); } diff --git a/frontend/src/components/settings/pages/general/Updater.tsx b/frontend/src/components/settings/pages/general/Updater.tsx index 59756a57..89b6d6ee 100644 --- a/frontend/src/components/settings/pages/general/Updater.tsx +++ b/frontend/src/components/settings/pages/general/Updater.tsx @@ -6,8 +6,8 @@ import { Focusable, ProgressBarWithInfo, Spinner, - findSP, showModal, + useWindowRef, } from '@decky/ui'; import { Suspense, lazy, useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -21,45 +21,48 @@ import WithSuspense from '../../../WithSuspense'; const MarkdownRenderer = lazy(() => import('../../../Markdown')); function PatchNotesModal({ versionInfo, closeModal }: { versionInfo: VerInfo | null; closeModal?: () => {} }) { - const SP = findSP(); + const [outerRef, win] = useWindowRef(); const { t } = useTranslation(); + // TODO proper desktop scrolling return ( - + - ( - -
-

{versionInfo?.all?.[id]?.name || 'Invalid Update Name'}

- {versionInfo?.all?.[id]?.body ? ( - - {versionInfo.all[id].body} - - ) : ( - t('Updater.no_patch_notes_desc') - )} -
-
- )} - fnGetId={(id) => id} - nNumItems={versionInfo?.all?.length} - nHeight={SP.innerHeight - 40} - nItemHeight={SP.innerHeight - 40} - nItemMarginX={0} - initialColumn={0} - autoFocus={true} - fnGetColumnWidth={() => SP.innerWidth} - name={t('Updater.decky_updates') as string} - /> + {win && ( + ( + +
+

{versionInfo?.all?.[id]?.name || 'Invalid Update Name'}

+ {versionInfo?.all?.[id]?.body ? ( + + {versionInfo.all[id].body} + + ) : ( + t('Updater.no_patch_notes_desc') + )} +
+
+ )} + fnGetId={(id) => id} + nNumItems={versionInfo?.all?.length} + nHeight={(win?.innerHeight || 800) - 40} + nItemHeight={(win?.innerHeight || 800) - 40} + nItemMarginX={0} + initialColumn={0} + autoFocus={true} + fnGetColumnWidth={() => win?.innerHeight || 1280} + name={t('Updater.decky_updates') as string} + /> + )}
); @@ -72,6 +75,8 @@ export default function UpdaterSettings() { const [updateProgress, setUpdateProgress] = useState(-1); const [reloading, setReloading] = useState(false); + const [windowRef, win] = useWindowRef(); + const { t } = useTranslation(); useEffect(() => { @@ -91,11 +96,12 @@ export default function UpdaterSettings() { }, []); const showPatchNotes = useCallback(() => { - showModal(); - }, [versionInfo]); + // TODO set width and height on desktop - needs fixing in DFL? + showModal(, win!); + }, [versionInfo, win]); return ( - <> +
)} - +
); } -- cgit v1.2.3