Added log viewer as side-tab in settings

This commit is contained in:
marios8543
2024-02-22 14:07:59 +02:00
parent 922d0c4153
commit 6b5f7c8642
7 changed files with 401 additions and 109 deletions
@@ -0,0 +1,48 @@
import {
DialogButton,
Focusable,
showModal,
} from "decky-frontend-lib";
import { FC, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import LogViewModal from "./LogViewModal";
const LogList: FC<{ plugin: string }> = ({ plugin }) => {
const [logList, setLogList] = useState([]);
const { t } = useTranslation();
useEffect(() => {
window.DeckyPluginLoader.callServerMethod("get_plugin_logs", {
plugin_name: plugin,
}).then((log_list) => {
setLogList(log_list.result || []);
});
}, []);
return (
<Focusable>
{logList.map((log_file) => (
<DialogButton
style={{ marginBottom: "0.5rem" }}
onOKActionDescription={t("LogViewer.viewLog", "View Log")}
onOKButton={() =>
showModal(
<LogViewModal name={log_file} plugin={plugin}></LogViewModal>,
)
}
onClick={() =>
showModal(
<LogViewModal name={log_file} plugin={plugin}></LogViewModal>,
)
}
>
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
<div>{log_file}</div>
</div>
</DialogButton>
))}
</Focusable>
);
};
export default LogList;
@@ -0,0 +1,45 @@
import { Focusable } from "decky-frontend-lib";
import { VFC, useEffect, useState } from "react";
import { ScrollableWindowRelative } from "./ScrollableWindow";
interface LogFileProps {
plugin: string;
name: string;
closeModal?: () => void;
}
const LogViewModal: VFC<LogFileProps> = ({ name, plugin, closeModal }) => {
const [logText, setLogText] = useState("Loading text....");
useEffect(() => {
window.DeckyPluginLoader.callServerMethod("get_plugin_log_text", {
plugin_name: plugin,
log_name: name,
}).then((text) => {
setLogText(text.result || "Error loading text");
});
}, []);
return (
<Focusable
style={{
padding: "0 15px",
display: "flex",
position: "absolute",
top: "var(--basicui-header-height)",
bottom: "var(--gamepadui-current-footer-height)",
left: 0,
right: 0,
}}
onSecondaryActionDescription={"Upload Log"}
onSecondaryButton={() => console.log("Uploading...")}
>
<ScrollableWindowRelative alwaysFocus={true} onCancel={closeModal}>
<div style={{ whiteSpace: "pre-wrap", padding: "12px 0" }}>
{logText}
</div>
</ScrollableWindowRelative>
</Focusable>
);
};
export default LogViewModal;
@@ -0,0 +1,35 @@
import { Focusable } from "decky-frontend-lib";
import { VFC, useState } from "react";
import { FaArrowDown, FaArrowUp } from "react-icons/fa";
import LogList from "./LogList";
interface LoggedPluginProps {
plugin: string;
}
const focusableStyle = {
background: "rgba(255,255,255,.15)",
borderRadius: "var(--round-radius-size)",
padding: "10px 24px",
marginBottom: "0.5rem",
};
const LoggedPlugin: VFC<LoggedPluginProps> = ({ plugin }) => {
const [isOpen, setOpen] = useState<boolean>(false);
return (
<div style={focusableStyle}>
<Focusable onOKButton={() => setOpen(!isOpen)}>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<div style={{ flexGrow: 1, textAlign: "left" }}>{plugin}</div>
<div style={{ textAlign: "right" }}>
{isOpen ? <FaArrowUp /> : <FaArrowDown />}
</div>
</div>
</Focusable>
{isOpen && <LogList plugin={plugin} />}
</div>
);
};
export default LoggedPlugin;
@@ -0,0 +1,107 @@
/*
Big thanks to @jessebofil for this
https://discord.com/channels/960281551428522045/960284327445418044/1209253688363716648
*/
import { Focusable, ModalPosition, GamepadButton, ScrollPanelGroup, gamepadDialogClasses, scrollPanelClasses, FooterLegendProps } from "decky-frontend-lib";
import { FC, useLayoutEffect, useRef, useState } from "react";
export interface ScrollableWindowProps extends FooterLegendProps {
height: string;
fadeAmount?: string;
scrollBarWidth?: string;
alwaysFocus?: boolean;
noScrollDescription?: boolean;
onActivate?: (e: CustomEvent) => void;
onCancel?: (e: CustomEvent) => void;
}
const ScrollableWindow: FC<ScrollableWindowProps> = ({ height, fadeAmount, scrollBarWidth, alwaysFocus, noScrollDescription, children, actionDescriptionMap, ...focusableProps }) => {
const fade = fadeAmount === undefined || fadeAmount === '' ? '10px' : fadeAmount;
const barWidth = scrollBarWidth === undefined || scrollBarWidth === '' ? '4px' : scrollBarWidth;
const [isOverflowing, setIsOverflowing] = useState(false);
const scrollPanelRef = useRef<HTMLElement>();
useLayoutEffect(() => {
const { current } = scrollPanelRef;
const trigger = () => {
if (current) {
const hasOverflow = current.scrollHeight > current.clientHeight;
setIsOverflowing(hasOverflow);
}
};
if (current) trigger();
}, [children, height]);
const panel = (
<ScrollPanelGroup
//@ts-ignore
ref={scrollPanelRef} focusable={false} style={{ flex: 1, minHeight: 0 }}>
<Focusable
//@ts-ignore
focusable={alwaysFocus || isOverflowing}
key={'scrollable-window-focusable-element'}
noFocusRing={true}
actionDescriptionMap={Object.assign(noScrollDescription ? {} :
{
[GamepadButton.DIR_UP]: 'Scroll Up',
[GamepadButton.DIR_DOWN]: 'Scroll Down'
},
actionDescriptionMap ?? {}
)}
{...focusableProps}
>
{children}
</Focusable>
</ScrollPanelGroup>
);
return (
<>
<style>
{`.modal-position-container .${gamepadDialogClasses.ModalPosition} {
top: 0;
bottom: 0;
padding: 0;
}
.modal-position-container .${scrollPanelClasses.ScrollPanel}::-webkit-scrollbar {
display: initial !important;
width: ${barWidth};
}
.modal-position-container .${scrollPanelClasses.ScrollPanel}::-webkit-scrollbar-thumb {
border: 0;
}`}
</style>
<div
className='modal-position-container'
style={{
position: 'relative',
height: height,
WebkitMask: `linear-gradient(to right , transparent, transparent calc(100% - ${barWidth}), white calc(100% - ${barWidth})), linear-gradient(to bottom, transparent, black ${fade}, black calc(100% - ${fade}), transparent 100%)`
}}>
{isOverflowing ? (
<ModalPosition key={'scrollable-window-modal-position'}>
{panel}
</ModalPosition>
) : (
<div className={`${gamepadDialogClasses.ModalPosition} ${gamepadDialogClasses.WithStandardPadding} Panel`} key={'modal-position'}>
{panel}
</div>
)}
</div>
</>
);
};
interface ScrollableWindowAutoProps extends Omit<ScrollableWindowProps, 'height'> {
heightPercent?: number;
}
export const ScrollableWindowRelative: FC<ScrollableWindowAutoProps> = ({ heightPercent, ...props }) => {
return (
<div style={{ flex: 'auto' }}>
<ScrollableWindow height={`${heightPercent ?? 100}%`} {...props} />
</div>
);
};
@@ -0,0 +1,20 @@
import { DialogBody } from 'decky-frontend-lib';
import { FC, useEffect, useState } from 'react';
import LoggedPlugin from './LoggedPlugin';
const LogViewerPage: FC<{}> = () => {
const [plugins, setPlugins] = useState([]);
useEffect(() => {
window.DeckyPluginLoader.callServerMethod('get_plugins_with_logs').then((plugins) => {
setPlugins(plugins.result || []);
});
}, []);
return (
<DialogBody>
{plugins.map((plugin) => <LoggedPlugin plugin={plugin} />)}
</DialogBody>
)
};
export default LogViewerPage;
+13 -2
View File
@@ -1,13 +1,14 @@
import { SidebarNavigation } from 'decky-frontend-lib';
import { lazy } from 'react';
import { useTranslation } from 'react-i18next';
import { FaCode, FaFlask, FaPlug } from 'react-icons/fa';
import { FaCode, FaFileCode, FaFlask, FaPlug } from 'react-icons/fa';
import { useSetting } from '../../utils/hooks/useSetting';
import DeckyIcon from '../DeckyIcon';
import WithSuspense from '../WithSuspense';
import GeneralSettings from './pages/general';
import PluginList from './pages/plugin_list';
import LogViewerPage from '../logviewer';
const DeveloperSettings = lazy(() => import('./pages/developer'));
const TestingMenu = lazy(() => import('./pages/testing'));
@@ -29,6 +30,16 @@ export default function SettingsPage() {
route: '/decky/settings/plugins',
icon: <FaPlug />,
},
{
title: t('SettingsIndex.log_viewer', "Log Viewer"),
content: (
<WithSuspense>
<LogViewerPage/>
</WithSuspense>
),
route: '/decky/settings/logs',
icon: <FaFileCode />
},
{
title: t('SettingsIndex.developer_title'),
content: (
@@ -50,7 +61,7 @@ export default function SettingsPage() {
route: '/decky/settings/testing',
icon: <FaFlask />,
visible: isDeveloper,
},
}
];
return <SidebarNavigation pages={pages} />;