@decky/ui 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +504 -0
- package/README.md +27 -0
- package/dist/class-mapper.d.ts +7 -0
- package/dist/class-mapper.js +26 -0
- package/dist/components/Button.d.ts +5 -0
- package/dist/components/Button.js +2 -0
- package/dist/components/ButtonItem.d.ts +7 -0
- package/dist/components/ButtonItem.js +3 -0
- package/dist/components/Carousel.d.ts +19 -0
- package/dist/components/Carousel.js +2 -0
- package/dist/components/ControlsList.d.ts +6 -0
- package/dist/components/ControlsList.js +2 -0
- package/dist/components/Dialog.d.ts +32 -0
- package/dist/components/Dialog.js +19 -0
- package/dist/components/DialogCheckbox.d.ts +17 -0
- package/dist/components/DialogCheckbox.js +14 -0
- package/dist/components/Dropdown.d.ts +34 -0
- package/dist/components/Dropdown.js +3 -0
- package/dist/components/Field.d.ts +22 -0
- package/dist/components/Field.js +2 -0
- package/dist/components/FocusRing.d.ts +9 -0
- package/dist/components/FocusRing.js +2 -0
- package/dist/components/Focusable.d.ts +12 -0
- package/dist/components/Focusable.js +2 -0
- package/dist/components/FooterLegend.d.ts +66 -0
- package/dist/components/FooterLegend.js +32 -0
- package/dist/components/Item.d.ts +11 -0
- package/dist/components/Item.js +1 -0
- package/dist/components/Marquee.d.ts +14 -0
- package/dist/components/Marquee.js +2 -0
- package/dist/components/Menu.d.ts +29 -0
- package/dist/components/Menu.js +8 -0
- package/dist/components/Modal.d.ts +52 -0
- package/dist/components/Modal.js +29 -0
- package/dist/components/Panel.d.ts +11 -0
- package/dist/components/Panel.js +4 -0
- package/dist/components/ProgressBar.d.ts +21 -0
- package/dist/components/ProgressBar.js +4 -0
- package/dist/components/Scroll.d.ts +7 -0
- package/dist/components/Scroll.js +5 -0
- package/dist/components/SidebarNavigation.d.ts +21 -0
- package/dist/components/SidebarNavigation.js +2 -0
- package/dist/components/SliderField.d.ts +26 -0
- package/dist/components/SliderField.js +2 -0
- package/dist/components/Spinner.d.ts +2 -0
- package/dist/components/Spinner.js +2 -0
- package/dist/components/SteamSpinner.d.ts +2 -0
- package/dist/components/SteamSpinner.js +2 -0
- package/dist/components/Tabs.d.ts +16 -0
- package/dist/components/Tabs.js +48 -0
- package/dist/components/TextField.d.ts +22 -0
- package/dist/components/TextField.js +2 -0
- package/dist/components/Toggle.d.ts +8 -0
- package/dist/components/Toggle.js +2 -0
- package/dist/components/ToggleField.d.ts +9 -0
- package/dist/components/ToggleField.js +2 -0
- package/dist/components/index.d.ts +25 -0
- package/dist/components/index.js +25 -0
- package/dist/custom-components/ColorPickerModal.d.ts +12 -0
- package/dist/custom-components/ColorPickerModal.js +87 -0
- package/dist/custom-components/ReorderableList.d.ts +26 -0
- package/dist/custom-components/ReorderableList.js +84 -0
- package/dist/custom-components/SuspensefulImage.d.ts +7 -0
- package/dist/custom-components/SuspensefulImage.js +27 -0
- package/dist/custom-components/index.d.ts +3 -0
- package/dist/custom-components/index.js +3 -0
- package/dist/custom-hooks/index.d.ts +1 -0
- package/dist/custom-hooks/index.js +1 -0
- package/dist/custom-hooks/useQuickAccessVisible.d.ts +1 -0
- package/dist/custom-hooks/useQuickAccessVisible.js +22 -0
- package/dist/deck-hooks/index.d.ts +1 -0
- package/dist/deck-hooks/index.js +1 -0
- package/dist/deck-hooks/useParams.d.ts +1 -0
- package/dist/deck-hooks/useParams.js +2 -0
- package/dist/globals/SteamClient.d.ts +307 -0
- package/dist/globals/SteamClient.js +1 -0
- package/dist/globals/index.d.ts +2 -0
- package/dist/globals/index.js +2 -0
- package/dist/globals/stores.d.ts +50 -0
- package/dist/globals/stores.js +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/logger.d.ts +17 -0
- package/dist/logger.js +46 -0
- package/dist/modules/Router.d.ts +110 -0
- package/dist/modules/Router.js +102 -0
- package/dist/modules/index.d.ts +1 -0
- package/dist/modules/index.js +1 -0
- package/dist/plugin.d.ts +68 -0
- package/dist/plugin.js +5 -0
- package/dist/utils/index.d.ts +12 -0
- package/dist/utils/index.js +23 -0
- package/dist/utils/patcher.d.ts +18 -0
- package/dist/utils/patcher.js +103 -0
- package/dist/utils/react.d.ts +18 -0
- package/dist/utils/react.js +74 -0
- package/dist/utils/static-classes.d.ts +52 -0
- package/dist/utils/static-classes.js +28 -0
- package/dist/webpack.d.ts +21 -0
- package/dist/webpack.js +102 -0
- package/package.json +82 -0
- package/src/class-mapper.ts +34 -0
- package/src/components/Button.tsx +8 -0
- package/src/components/ButtonItem.tsx +14 -0
- package/src/components/Carousel.ts +25 -0
- package/src/components/ControlsList.tsx +13 -0
- package/src/components/Dialog.tsx +86 -0
- package/src/components/DialogCheckbox.tsx +36 -0
- package/src/components/Dropdown.tsx +49 -0
- package/src/components/Field.tsx +28 -0
- package/src/components/FocusRing.ts +15 -0
- package/src/components/Focusable.tsx +18 -0
- package/src/components/FooterLegend.ts +67 -0
- package/src/components/Item.tsx +12 -0
- package/src/components/Marquee.tsx +20 -0
- package/src/components/Menu.tsx +57 -0
- package/src/components/Modal.tsx +119 -0
- package/src/components/Panel.tsx +26 -0
- package/src/components/ProgressBar.tsx +35 -0
- package/src/components/Scroll.tsx +15 -0
- package/src/components/SidebarNavigation.tsx +28 -0
- package/src/components/SliderField.tsx +33 -0
- package/src/components/Spinner.tsx +8 -0
- package/src/components/SteamSpinner.tsx +7 -0
- package/src/components/Tabs.tsx +127 -0
- package/src/components/TextField.tsx +28 -0
- package/src/components/Toggle.tsx +14 -0
- package/src/components/ToggleField.tsx +15 -0
- package/src/components/index.ts +25 -0
- package/src/custom-components/ColorPickerModal.tsx +132 -0
- package/src/custom-components/ReorderableList.tsx +183 -0
- package/src/custom-components/SuspensefulImage.tsx +44 -0
- package/src/custom-components/index.ts +3 -0
- package/src/custom-hooks/index.ts +1 -0
- package/src/custom-hooks/useQuickAccessVisible.tsx +63 -0
- package/src/deck-hooks/index.ts +1 -0
- package/src/deck-hooks/useParams.ts +15 -0
- package/src/globals/SteamClient.ts +321 -0
- package/src/globals/index.ts +2 -0
- package/src/globals/stores.ts +50 -0
- package/src/index.ts +11 -0
- package/src/logger.ts +77 -0
- package/src/modules/Router.tsx +174 -0
- package/src/modules/index.ts +1 -0
- package/src/plugin.tsx +96 -0
- package/src/utils/index.ts +43 -0
- package/src/utils/patcher.ts +160 -0
- package/src/utils/react.ts +119 -0
- package/src/utils/static-classes.ts +1104 -0
- package/src/webpack.ts +136 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { FC, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { findSP } from '../utils';
|
|
4
|
+
import { Export, findModule, findModuleByExport, findModuleExport } from '../webpack';
|
|
5
|
+
|
|
6
|
+
// All of the popout options + strTitle are related. Proper usage is not yet known...
|
|
7
|
+
export interface ShowModalProps {
|
|
8
|
+
browserContext?: unknown;
|
|
9
|
+
bForcePopOut?: boolean;
|
|
10
|
+
bHideActionIcons?: boolean;
|
|
11
|
+
bHideMainWindowForPopouts?: boolean;
|
|
12
|
+
bNeverPopOut?: boolean;
|
|
13
|
+
fnOnClose?: () => void; // Seems to be the same as "closeModal" callback, but only when the modal is a popout. Will no longer work after "Update" invocation!
|
|
14
|
+
popupHeight?: number;
|
|
15
|
+
popupWidth?: number;
|
|
16
|
+
promiseRenderComplete?: Promise<void>; // Invoked once the render is complete. Currently, it seems to be used as image loading success/error callback...
|
|
17
|
+
strTitle?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ShowModalResult {
|
|
21
|
+
// This method will not invoke any of the variations of "closeModal" callbacks!
|
|
22
|
+
Close: () => void;
|
|
23
|
+
|
|
24
|
+
// This method will replace the modal element completely and will not update the callback chains,
|
|
25
|
+
// meaning that "closeModal" and etc. will not automatically close the modal anymore (also "fnOnClose"
|
|
26
|
+
// will not be even called upon close anymore)! You have to manually call the "Close" method when, for example,
|
|
27
|
+
// the "closeModal" is invoked in the newly updated modal:
|
|
28
|
+
// <ModalRoot closeModal={() => { console.log("ABOUT TO CLOSE"); showModalRes.Close(); }} />
|
|
29
|
+
Update: (modal: ReactNode) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const showModalRaw: (
|
|
33
|
+
modal: ReactNode,
|
|
34
|
+
parent?: EventTarget,
|
|
35
|
+
title?: string,
|
|
36
|
+
props?: ShowModalProps,
|
|
37
|
+
unknown1?: unknown,
|
|
38
|
+
hideActions?: { bHideActions?: boolean },
|
|
39
|
+
modalManager?: unknown,
|
|
40
|
+
) => ShowModalResult = findModuleExport(
|
|
41
|
+
(e: Export) =>
|
|
42
|
+
typeof e === 'function' && e.toString().includes('props.bDisableBackgroundDismiss') && !e?.prototype?.Cancel,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
export const showModal = (
|
|
46
|
+
modal: ReactNode,
|
|
47
|
+
parent?: EventTarget,
|
|
48
|
+
props: ShowModalProps = {
|
|
49
|
+
strTitle: 'Decky Dialog',
|
|
50
|
+
bHideMainWindowForPopouts: false,
|
|
51
|
+
},
|
|
52
|
+
): ShowModalResult => {
|
|
53
|
+
return showModalRaw(modal, parent || findSP(), props.strTitle, props, undefined, {
|
|
54
|
+
bHideActions: props.bHideActionIcons,
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export interface ModalRootProps {
|
|
59
|
+
children?: ReactNode;
|
|
60
|
+
onCancel?(): void;
|
|
61
|
+
closeModal?(): void;
|
|
62
|
+
onOK?(): void;
|
|
63
|
+
onEscKeypress?(): void;
|
|
64
|
+
className?: string;
|
|
65
|
+
modalClassName?: string;
|
|
66
|
+
bAllowFullSize?: boolean;
|
|
67
|
+
bDestructiveWarning?: boolean;
|
|
68
|
+
bDisableBackgroundDismiss?: boolean;
|
|
69
|
+
bHideCloseIcon?: boolean;
|
|
70
|
+
bOKDisabled?: boolean;
|
|
71
|
+
bCancelDisabled?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ConfirmModalProps extends ModalRootProps {
|
|
75
|
+
onMiddleButton?(): void; // setting this prop will enable the middle button
|
|
76
|
+
strTitle?: ReactNode;
|
|
77
|
+
strDescription?: ReactNode;
|
|
78
|
+
strOKButtonText?: ReactNode;
|
|
79
|
+
strCancelButtonText?: ReactNode;
|
|
80
|
+
strMiddleButtonText?: ReactNode;
|
|
81
|
+
bAlertDialog?: boolean; // This will open a modal with only OK button enabled
|
|
82
|
+
bMiddleDisabled?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const ConfirmModal = findModuleExport(
|
|
86
|
+
(e: Export) => !e?.prototype?.OK && e?.prototype?.Cancel && e?.prototype?.render,
|
|
87
|
+
) as FC<ConfirmModalProps>;
|
|
88
|
+
|
|
89
|
+
export const ModalRoot = Object.values(
|
|
90
|
+
findModule((m: any) => {
|
|
91
|
+
if (typeof m !== 'object') return false;
|
|
92
|
+
|
|
93
|
+
for (let prop in m) {
|
|
94
|
+
if (m[prop]?.m_mapModalManager && Object.values(m)?.find((x: any) => x?.type)) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return false;
|
|
100
|
+
}) || {},
|
|
101
|
+
)?.find((x: any) => x?.type?.toString()?.includes('((function(){')) as FC<ModalRootProps>;
|
|
102
|
+
|
|
103
|
+
interface SimpleModalProps {
|
|
104
|
+
active?: boolean;
|
|
105
|
+
children: ReactNode;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const ModalModule = findModuleByExport((e: Export) => e?.toString().includes('.ModalPosition,fallback:'), 5);
|
|
109
|
+
|
|
110
|
+
const ModalModuleProps = ModalModule ? Object.values(ModalModule) : [];
|
|
111
|
+
|
|
112
|
+
export const SimpleModal = ModalModuleProps.find((prop) => {
|
|
113
|
+
const string = prop?.toString();
|
|
114
|
+
return string?.includes('.ShowPortalModal()') && string?.includes('.OnElementReadyCallbacks.Register(');
|
|
115
|
+
}) as FC<SimpleModalProps>;
|
|
116
|
+
|
|
117
|
+
export const ModalPosition = ModalModuleProps.find((prop) =>
|
|
118
|
+
prop?.toString().includes('.ModalPosition,fallback:'),
|
|
119
|
+
) as FC<SimpleModalProps>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { FC, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Export, findModuleDetailsByExport } from '../webpack';
|
|
4
|
+
|
|
5
|
+
// TODO where did this go?
|
|
6
|
+
// export const Panel: FC<{ children?: ReactNode; }> = findModuleExport((e: Export) => {
|
|
7
|
+
// if (typeof mod !== 'object' || !mod.__esModule) return undefined;
|
|
8
|
+
// return mod.Panel;
|
|
9
|
+
// });
|
|
10
|
+
|
|
11
|
+
export interface PanelSectionProps {
|
|
12
|
+
title?: string;
|
|
13
|
+
spinner?: boolean;
|
|
14
|
+
children?: ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const [mod, panelSection] = findModuleDetailsByExport((e: Export) => e.toString()?.includes('.PanelSection'));
|
|
18
|
+
|
|
19
|
+
export const PanelSection = panelSection as FC<PanelSectionProps>;
|
|
20
|
+
|
|
21
|
+
export interface PanelSectionRowProps {
|
|
22
|
+
children?: ReactNode;
|
|
23
|
+
}
|
|
24
|
+
export const PanelSectionRow = Object.values(mod).filter(
|
|
25
|
+
(exp: any) => !exp?.toString()?.includes('.PanelSection'),
|
|
26
|
+
)[0] as FC<PanelSectionRowProps>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ReactNode, VFC } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Export, findModuleExport } from '../webpack';
|
|
4
|
+
import { ItemProps } from './Item';
|
|
5
|
+
|
|
6
|
+
export interface ProgressBarItemProps extends ItemProps {
|
|
7
|
+
indeterminate?: boolean;
|
|
8
|
+
nTransitionSec?: number;
|
|
9
|
+
nProgress?: number;
|
|
10
|
+
focusable?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ProgressBarProps {
|
|
14
|
+
indeterminate?: boolean;
|
|
15
|
+
nTransitionSec?: number;
|
|
16
|
+
nProgress?: number;
|
|
17
|
+
focusable?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ProgressBarWithInfoProps extends ProgressBarItemProps {
|
|
21
|
+
sTimeRemaining?: ReactNode;
|
|
22
|
+
sOperationText?: ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const ProgressBar = findModuleExport((e: Export) =>
|
|
26
|
+
e?.toString()?.includes('.ProgressBar,"standard"=='),
|
|
27
|
+
) as VFC<ProgressBarProps>;
|
|
28
|
+
|
|
29
|
+
export const ProgressBarWithInfo = findModuleExport((e: Export) =>
|
|
30
|
+
e?.toString()?.includes('.ProgressBarFieldStatus},'),
|
|
31
|
+
) as VFC<ProgressBarWithInfoProps>;
|
|
32
|
+
|
|
33
|
+
export const ProgressBarItem = findModuleExport((e: Export) =>
|
|
34
|
+
e?.toString()?.includes('"indeterminate","nTransitionSec"'),
|
|
35
|
+
) as VFC<ProgressBarItemProps>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { FC, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Export, findModuleByExport, findModuleExport } from '../webpack';
|
|
4
|
+
|
|
5
|
+
const ScrollingModule = findModuleByExport((e: Export) => e?.render?.toString?.().includes('{case"x":'));
|
|
6
|
+
|
|
7
|
+
const ScrollingModuleProps = ScrollingModule ? Object.values(ScrollingModule) : [];
|
|
8
|
+
|
|
9
|
+
export const ScrollPanel = ScrollingModuleProps.find((prop: any) =>
|
|
10
|
+
prop?.render?.toString?.().includes('{case"x":'),
|
|
11
|
+
) as FC<{ children?: ReactNode }>;
|
|
12
|
+
|
|
13
|
+
export const ScrollPanelGroup: FC<{ children?: ReactNode }> = findModuleExport((e: Export) =>
|
|
14
|
+
e?.render?.toString().includes('.FocusVisibleChild()),[])'),
|
|
15
|
+
);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ReactNode, VFC } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Export, findModuleExport } from '../webpack';
|
|
4
|
+
|
|
5
|
+
export interface SidebarNavigationPage {
|
|
6
|
+
title: ReactNode;
|
|
7
|
+
content: ReactNode;
|
|
8
|
+
icon?: ReactNode;
|
|
9
|
+
visible?: boolean;
|
|
10
|
+
hideTitle?: boolean;
|
|
11
|
+
identifier?: string;
|
|
12
|
+
route?: string;
|
|
13
|
+
link?: string;
|
|
14
|
+
padding?: 'none' | 'compact';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SidebarNavigationProps {
|
|
18
|
+
title?: string;
|
|
19
|
+
pages: (SidebarNavigationPage | 'separator')[];
|
|
20
|
+
showTitle?: boolean;
|
|
21
|
+
disableRouteReporting?: boolean;
|
|
22
|
+
page?: string;
|
|
23
|
+
onPageRequested?: (page: string) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const SidebarNavigation = findModuleExport((e: Export) =>
|
|
27
|
+
e?.toString()?.includes('"disableRouteReporting"'),
|
|
28
|
+
) as VFC<SidebarNavigationProps>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
|
|
3
|
+
import { CommonUIModule } from '../webpack';
|
|
4
|
+
import { ItemProps } from './Item';
|
|
5
|
+
|
|
6
|
+
export interface NotchLabel {
|
|
7
|
+
notchIndex: number;
|
|
8
|
+
label: string;
|
|
9
|
+
value?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SliderFieldProps extends ItemProps {
|
|
13
|
+
value: number;
|
|
14
|
+
min?: number;
|
|
15
|
+
max?: number;
|
|
16
|
+
step?: number;
|
|
17
|
+
notchCount?: number;
|
|
18
|
+
notchLabels?: NotchLabel[];
|
|
19
|
+
notchTicksVisible?: boolean;
|
|
20
|
+
showValue?: boolean;
|
|
21
|
+
resetValue?: number;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
editableValue?: boolean;
|
|
24
|
+
validValues?: 'steps' | 'range' | ((value: number) => boolean);
|
|
25
|
+
valueSuffix?: string;
|
|
26
|
+
minimumDpadGranularity?: number;
|
|
27
|
+
onChange?(value: number): void;
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const SliderField = Object.values(CommonUIModule).find((mod: any) =>
|
|
32
|
+
mod?.toString()?.includes('SliderField,fallback'),
|
|
33
|
+
) as FC<SliderFieldProps>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { FC, SVGAttributes } from 'react';
|
|
2
|
+
|
|
3
|
+
import { IconsModule } from '../webpack';
|
|
4
|
+
|
|
5
|
+
// TODO type this and other icons?
|
|
6
|
+
export const Spinner = Object.values(IconsModule).find(
|
|
7
|
+
(mod: any) => mod?.toString && /Spinner\)}\),.\.createElement\(\"path\",{d:\"M18 /.test(mod.toString()),
|
|
8
|
+
) as FC<SVGAttributes<SVGElement>>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { FC, SVGAttributes } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Export, findModuleExport } from '../webpack';
|
|
4
|
+
|
|
5
|
+
export const SteamSpinner = findModuleExport(
|
|
6
|
+
(e: Export) => e?.toString?.()?.includes('Steam Spinner') && e?.toString?.()?.includes('src'),
|
|
7
|
+
) as FC<SVGAttributes<SVGElement>>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { FC, ReactNode, createElement, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { fakeRenderComponent, findInReactTree, sleep } from '../utils';
|
|
4
|
+
import { Export, findModuleByExport } from '../webpack';
|
|
5
|
+
import { FooterLegendProps } from './FooterLegend';
|
|
6
|
+
import { SteamSpinner } from './SteamSpinner';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Individual tab objects for the Tabs component
|
|
10
|
+
*
|
|
11
|
+
* `id` ID of this tab, can be used with activeTab to auto-focus a given tab
|
|
12
|
+
* `title` Title shown in the header bar
|
|
13
|
+
* `renderTabAddon` Return a {@link ReactNode} to render it next to the tab title, i.e. the counts for each tab on the Media page
|
|
14
|
+
* `content` Content of the tab
|
|
15
|
+
* `footer` Sets up button handlers and labels
|
|
16
|
+
*/
|
|
17
|
+
export interface Tab {
|
|
18
|
+
id: string;
|
|
19
|
+
title: string;
|
|
20
|
+
renderTabAddon?: () => ReactNode;
|
|
21
|
+
content: ReactNode;
|
|
22
|
+
footer?: FooterLegendProps;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Props for the {@link Tabs}
|
|
27
|
+
*
|
|
28
|
+
* `tabs` array of {@link Tab}
|
|
29
|
+
* `activeTab` tab currently active, needs to be one of the tabs {@link Tab.id}, must be set using a `useState` in the `onShowTab` handler
|
|
30
|
+
* `onShowTab` Called when the active tab should change, needs to set `activeTab`. See example.
|
|
31
|
+
* `autoFocusContents` Whether to automatically focus the tab contents or not.
|
|
32
|
+
* `footer` Sets up button handlers and labels
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* const Component: FC = () => {
|
|
36
|
+
* const [currentTab, setCurrentTab] = useState<string>("Tab1");
|
|
37
|
+
*
|
|
38
|
+
* return (
|
|
39
|
+
* <Tabs
|
|
40
|
+
* title="Theme Manager"
|
|
41
|
+
* activeTab={currentTabRoute}
|
|
42
|
+
* onShowTab={(tabID: string) => {
|
|
43
|
+
* setCurrentTabRoute(tabID);
|
|
44
|
+
* }}
|
|
45
|
+
* tabs={[
|
|
46
|
+
* {
|
|
47
|
+
* title: "Tab 1",
|
|
48
|
+
* content: <Tab1Component />,
|
|
49
|
+
* id: "Tab1",
|
|
50
|
+
* },
|
|
51
|
+
* {
|
|
52
|
+
* title: "Tab 2",
|
|
53
|
+
* content: <Tab2Component />,
|
|
54
|
+
* id: "Tab2",
|
|
55
|
+
* },
|
|
56
|
+
* ]}
|
|
57
|
+
* />
|
|
58
|
+
* );
|
|
59
|
+
* };
|
|
60
|
+
*/
|
|
61
|
+
export interface TabsProps {
|
|
62
|
+
tabs: Tab[];
|
|
63
|
+
activeTab: string;
|
|
64
|
+
onShowTab: (tab: string) => void;
|
|
65
|
+
autoFocusContents?: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let tabsComponent: any;
|
|
69
|
+
|
|
70
|
+
const getTabs = async () => {
|
|
71
|
+
if (tabsComponent) return tabsComponent;
|
|
72
|
+
// @ts-ignore
|
|
73
|
+
while (!window?.DeckyPluginLoader?.routerHook?.routes) {
|
|
74
|
+
console.debug('[DFL:Tabs]: Waiting for Decky router...');
|
|
75
|
+
await sleep(500);
|
|
76
|
+
}
|
|
77
|
+
return (tabsComponent = fakeRenderComponent(
|
|
78
|
+
() => {
|
|
79
|
+
return findInReactTree(
|
|
80
|
+
findInReactTree(
|
|
81
|
+
// @ts-ignore
|
|
82
|
+
window.DeckyPluginLoader.routerHook.routes
|
|
83
|
+
.find((x: any) => x.props.path == '/library/app/:appid/achievements')
|
|
84
|
+
.props.children.type(),
|
|
85
|
+
(x) => x?.props?.scrollTabsTop,
|
|
86
|
+
).type({ appid: 1 }),
|
|
87
|
+
(x) => x?.props?.tabs,
|
|
88
|
+
).type;
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
useRef: () => ({ current: { reaction: { track: () => {} } } }),
|
|
92
|
+
useContext: () => ({ match: { params: { appid: 1 } } }),
|
|
93
|
+
useMemo: () => ({ data: {} }),
|
|
94
|
+
},
|
|
95
|
+
));
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
let oldTabs: any;
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const oldTabsModule = findModuleByExport((e: Export) => e.Unbleed);
|
|
102
|
+
if (oldTabsModule)
|
|
103
|
+
oldTabs = Object.values(oldTabsModule).find((x: any) => x?.type?.toString()?.includes('((function(){'));
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.error('Error finding oldTabs:', e);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Tabs component as used in the library and media tabs. See {@link TabsProps}.
|
|
110
|
+
* Unlike other components in `decky-frontend-lib`, this requires Decky Loader to be running.
|
|
111
|
+
*/
|
|
112
|
+
export const Tabs = (oldTabs ||
|
|
113
|
+
((props: TabsProps) => {
|
|
114
|
+
const found = tabsComponent;
|
|
115
|
+
const [tc, setTC] = useState<FC<TabsProps>>(found);
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (found) return;
|
|
118
|
+
(async () => {
|
|
119
|
+
console.debug('[DFL:Tabs]: Finding component...');
|
|
120
|
+
const t = await getTabs();
|
|
121
|
+
console.debug('[DFL:Tabs]: Found!');
|
|
122
|
+
setTC(t);
|
|
123
|
+
})();
|
|
124
|
+
}, []);
|
|
125
|
+
console.log('tc', tc);
|
|
126
|
+
return tc ? createElement(tc, props) : <SteamSpinner />;
|
|
127
|
+
})) as FC<TabsProps>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ChangeEventHandler, HTMLAttributes, ReactNode, VFC } from 'react';
|
|
2
|
+
|
|
3
|
+
import { CommonUIModule, Module } from '../webpack';
|
|
4
|
+
|
|
5
|
+
export interface TextFieldProps extends HTMLAttributes<HTMLInputElement> {
|
|
6
|
+
label?: ReactNode;
|
|
7
|
+
requiredLabel?: ReactNode;
|
|
8
|
+
description?: ReactNode;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
bShowCopyAction?: boolean;
|
|
11
|
+
bShowClearAction?: boolean;
|
|
12
|
+
bAlwaysShowClearAction?: boolean;
|
|
13
|
+
bIsPassword?: boolean;
|
|
14
|
+
rangeMin?: number;
|
|
15
|
+
rangeMax?: number;
|
|
16
|
+
mustBeNumeric?: boolean;
|
|
17
|
+
mustBeURL?: boolean;
|
|
18
|
+
mustBeEmail?: boolean;
|
|
19
|
+
focusOnMount?: boolean;
|
|
20
|
+
tooltip?: string;
|
|
21
|
+
inlineControls?: ReactNode;
|
|
22
|
+
onChange?: ChangeEventHandler<HTMLInputElement>;
|
|
23
|
+
value?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const TextField = Object.values(CommonUIModule).find(
|
|
27
|
+
(mod: Module) => mod?.validateUrl && mod?.validateEmail,
|
|
28
|
+
) as VFC<TextFieldProps>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
|
|
3
|
+
import { CommonUIModule } from '../webpack';
|
|
4
|
+
|
|
5
|
+
export interface ToggleProps {
|
|
6
|
+
value: boolean;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
onChange?(checked: boolean): void;
|
|
9
|
+
navRef?: any; // TODO figure out what this is
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const Toggle = Object.values(CommonUIModule).find((mod: any) =>
|
|
13
|
+
mod?.render?.toString()?.includes('.ToggleOff)'),
|
|
14
|
+
) as FC<ToggleProps>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
|
|
3
|
+
import { CommonUIModule } from '../webpack';
|
|
4
|
+
import { ItemProps } from './Item';
|
|
5
|
+
|
|
6
|
+
export interface ToggleFieldProps extends ItemProps {
|
|
7
|
+
highlightOnFocus?: boolean;
|
|
8
|
+
checked: boolean;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
onChange?(checked: boolean): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ToggleField = Object.values(CommonUIModule).find((mod: any) =>
|
|
14
|
+
mod?.render?.toString()?.includes('ToggleField,fallback'),
|
|
15
|
+
) as FC<ToggleFieldProps>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export * from './Button';
|
|
2
|
+
export * from './ButtonItem';
|
|
3
|
+
export * from './Carousel';
|
|
4
|
+
export * from './ControlsList';
|
|
5
|
+
export * from './Dialog';
|
|
6
|
+
export * from './DialogCheckbox';
|
|
7
|
+
export * from './Dropdown';
|
|
8
|
+
export * from './Field';
|
|
9
|
+
export * from './Focusable';
|
|
10
|
+
export * from './FocusRing';
|
|
11
|
+
export * from './FooterLegend';
|
|
12
|
+
export * from './Marquee';
|
|
13
|
+
export * from './Menu';
|
|
14
|
+
export * from './Modal';
|
|
15
|
+
export * from './Panel';
|
|
16
|
+
export * from './ProgressBar';
|
|
17
|
+
export * from './SidebarNavigation';
|
|
18
|
+
export * from './SliderField';
|
|
19
|
+
export * from './Spinner';
|
|
20
|
+
export * from './SteamSpinner';
|
|
21
|
+
export * from './Tabs';
|
|
22
|
+
export * from './TextField';
|
|
23
|
+
export * from './Toggle';
|
|
24
|
+
export * from './ToggleField';
|
|
25
|
+
export * from './Scroll';
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { CSSProperties, FC, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { ConfirmModal, SliderField } from '../components';
|
|
4
|
+
import { gamepadSliderClasses } from '../utils/static-classes';
|
|
5
|
+
|
|
6
|
+
interface ColorPickerModalProps {
|
|
7
|
+
closeModal: () => void;
|
|
8
|
+
onConfirm?(HSLString: string): any;
|
|
9
|
+
title?: string;
|
|
10
|
+
defaultH?: number;
|
|
11
|
+
defaultS?: number;
|
|
12
|
+
defaultL?: number;
|
|
13
|
+
defaultA?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const ColorPickerModal: FC<ColorPickerModalProps> = ({
|
|
17
|
+
closeModal,
|
|
18
|
+
onConfirm = () => {},
|
|
19
|
+
title = 'Color Picker',
|
|
20
|
+
defaultH = 0,
|
|
21
|
+
defaultS = 100,
|
|
22
|
+
defaultL = 50,
|
|
23
|
+
defaultA = 1,
|
|
24
|
+
}) => {
|
|
25
|
+
const [H, setH] = useState<number>(defaultH);
|
|
26
|
+
const [S, setS] = useState<number>(defaultS);
|
|
27
|
+
const [L, setL] = useState<number>(defaultL);
|
|
28
|
+
const [A, setA] = useState<number>(defaultA);
|
|
29
|
+
|
|
30
|
+
const colorPickerCSSVars = {
|
|
31
|
+
'--decky-color-picker-hvalue': `${H}`,
|
|
32
|
+
'--decky-color-picker-svalue': `${S}%`,
|
|
33
|
+
'--decky-color-picker-lvalue': `${L}%`,
|
|
34
|
+
'--decky-color-picker-avalue': `${A}`,
|
|
35
|
+
} as CSSProperties;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<ConfirmModal
|
|
39
|
+
bAllowFullSize
|
|
40
|
+
onCancel={closeModal}
|
|
41
|
+
onOK={() => {
|
|
42
|
+
onConfirm(`hsla(${H}, ${S}%, ${L}%, ${A})`);
|
|
43
|
+
closeModal();
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
<style>
|
|
47
|
+
{`
|
|
48
|
+
/* This removes the cyan track color that is behind the slider head */
|
|
49
|
+
.ColorPicker_Container .${gamepadSliderClasses.SliderTrack} {
|
|
50
|
+
--left-track-color: #0000;
|
|
51
|
+
/* This is for compatibility with the "Colored Toggles" CSSLoader Theme*/
|
|
52
|
+
--colored-toggles-main-color: #0000;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.ColorPicker_HSlider .${gamepadSliderClasses.SliderTrack} {
|
|
56
|
+
background: linear-gradient(
|
|
57
|
+
270deg,
|
|
58
|
+
hsla(360, var(--decky-color-picker-svalue), var(--decky-color-picker-lvalue), var(--decky-color-picker-avalue)),
|
|
59
|
+
hsla(270, var(--decky-color-picker-svalue), var(--decky-color-picker-lvalue), var(--decky-color-picker-avalue)),
|
|
60
|
+
hsla(180, var(--decky-color-picker-svalue), var(--decky-color-picker-lvalue), var(--decky-color-picker-avalue)),
|
|
61
|
+
hsla(90, var(--decky-color-picker-svalue), var(--decky-color-picker-lvalue), var(--decky-color-picker-avalue)),
|
|
62
|
+
hsla(0, var(--decky-color-picker-svalue), var(--decky-color-picker-lvalue), var(--decky-color-picker-avalue))
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.ColorPicker_SSlider .${gamepadSliderClasses.SliderTrack} {
|
|
67
|
+
background: linear-gradient(
|
|
68
|
+
90deg,
|
|
69
|
+
hsla(var(--decky-color-picker-hvalue), 0%, var(--decky-color-picker-lvalue), var(--decky-color-picker-avalue)),
|
|
70
|
+
hsla(var(--decky-color-picker-hvalue), 100%, var(--decky-color-picker-lvalue), var(--decky-color-picker-avalue))
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.ColorPicker_LSlider .${gamepadSliderClasses.SliderTrack} {
|
|
75
|
+
background: linear-gradient(
|
|
76
|
+
90deg,
|
|
77
|
+
hsla(var(--decky-color-picker-hvalue), var(--decky-color-picker-svalue), 0%, var(--decky-color-picker-avalue)),
|
|
78
|
+
hsla(var(--decky-color-picker-hvalue), var(--decky-color-picker-svalue), 50%, var(--decky-color-picker-avalue)),
|
|
79
|
+
hsla(var(--decky-color-picker-hvalue), var(--decky-color-picker-svalue), 100%, var(--decky-color-picker-avalue))
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.ColorPicker_ASlider .${gamepadSliderClasses.SliderTrack} {
|
|
84
|
+
background: linear-gradient(
|
|
85
|
+
90deg,
|
|
86
|
+
hsla(var(--decky-color-picker-hvalue), var(--decky-color-picker-svalue), var(--decky-color-picker-lvalue), 0),
|
|
87
|
+
hsla(var(--decky-color-picker-hvalue), var(--decky-color-picker-svalue), var(--decky-color-picker-lvalue), 1)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
`}
|
|
91
|
+
</style>
|
|
92
|
+
<div
|
|
93
|
+
className="ColorPicker_ColorDisplayContainer"
|
|
94
|
+
style={{
|
|
95
|
+
display: 'flex',
|
|
96
|
+
justifyContent: 'space-between',
|
|
97
|
+
alignItems: 'center',
|
|
98
|
+
marginBottom: '1em',
|
|
99
|
+
// theres a large header by default on the modal, so this just pushes it up into that unused space
|
|
100
|
+
marginTop: '-2.5em',
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<div>
|
|
104
|
+
<span style={{ fontSize: '1.5em' }}>
|
|
105
|
+
<b>{title}</b>
|
|
106
|
+
</span>
|
|
107
|
+
</div>
|
|
108
|
+
<div
|
|
109
|
+
style={{
|
|
110
|
+
backgroundColor: `hsla(${H}, ${S}%, ${L}%, ${A})`,
|
|
111
|
+
width: '40px',
|
|
112
|
+
height: '40px',
|
|
113
|
+
}}
|
|
114
|
+
></div>
|
|
115
|
+
</div>
|
|
116
|
+
<div className="ColorPicker_Container" style={colorPickerCSSVars}>
|
|
117
|
+
<div className="ColorPicker_HSlider">
|
|
118
|
+
<SliderField showValue editableValue label="Hue" value={H} min={0} max={360} onChange={setH} />
|
|
119
|
+
</div>
|
|
120
|
+
<div className="ColorPicker_SSlider">
|
|
121
|
+
<SliderField showValue editableValue label="Saturation" value={S} min={0} max={100} onChange={setS} />
|
|
122
|
+
</div>
|
|
123
|
+
<div className="ColorPicker_LSlider">
|
|
124
|
+
<SliderField showValue editableValue label="Lightness" value={L} min={0} max={100} onChange={setL} />
|
|
125
|
+
</div>
|
|
126
|
+
<div className="ColorPicker_ASlider">
|
|
127
|
+
<SliderField showValue editableValue label="Alpha" value={A} step={0.1} min={0} max={1} onChange={setA} />
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</ConfirmModal>
|
|
131
|
+
);
|
|
132
|
+
};
|