mirror of
https://github.com/SteamDeckHomebrew/decky-loader.git
synced 2026-06-17 08:47:49 +00:00
Added log viewer as side-tab in settings
This commit is contained in:
@@ -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;
|
||||
@@ -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} />;
|
||||
|
||||
Reference in New Issue
Block a user