mirror of
https://github.com/Equicord/Equicord.git
synced 2025-04-01 13:11:57 -04:00
Add files via upload
This commit is contained in:
parent
7c5f01bf11
commit
04f3a22c9b
6 changed files with 870 additions and 0 deletions
14
src/equicordplugins/consoleViewer/components/ConsoleIcon.tsx
Normal file
14
src/equicordplugins/consoleViewer/components/ConsoleIcon.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
export function ConsoleLogIcon() {
|
||||
return `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 4H4C2.9 4 2 4.9 2 6V18C2 19.1 2.9 20 4 20H20C21.1 20 22 19.1 22 18V6C22 4.9 21.1 4 20 4Z"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 12L10.5 14.5L8 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M16 17H13.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>`;
|
||||
}
|
323
src/equicordplugins/consoleViewer/components/ConsoleModal.tsx
Normal file
323
src/equicordplugins/consoleViewer/components/ConsoleModal.tsx
Normal file
|
@ -0,0 +1,323 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import ErrorBoundary from "@components/ErrorBoundary";
|
||||
import { Button, React, ReactDOM } from "@webpack/common";
|
||||
|
||||
import { clearLogs, downloadLogs, getCapturedLogs, LogEntry } from "../utils/consoleLogger";
|
||||
|
||||
const timeFormat = new Intl.DateTimeFormat(undefined, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false
|
||||
});
|
||||
const ErrorFallback = () => (
|
||||
<div className="console-viewer-error">Failed to load Console Viewer</div>
|
||||
);
|
||||
function ConsoleViewerModal() {
|
||||
const [logs, setLogs] = React.useState<LogEntry[]>([]);
|
||||
const [search, setSearch] = React.useState("");
|
||||
const [filter, setFilter] = React.useState("all");
|
||||
const [autoScroll, setAutoScroll] = React.useState(true);
|
||||
const logContainerRef = React.useRef<HTMLDivElement>(null);
|
||||
const modalRef = React.useRef<HTMLDivElement>(null);
|
||||
React.useEffect(() => {
|
||||
function updateLogs() {
|
||||
setLogs([...getCapturedLogs()]);
|
||||
}
|
||||
updateLogs();
|
||||
window.addEventListener("console-captured", updateLogs);
|
||||
return () => {
|
||||
window.removeEventListener("console-captured", updateLogs);
|
||||
};
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
if (autoScroll && logContainerRef.current) {
|
||||
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
|
||||
}
|
||||
}, [logs, autoScroll]);
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
closeConsoleViewer();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
|
||||
closeConsoleViewer();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
const filteredLogs = React.useMemo(() => {
|
||||
return logs.filter(log => {
|
||||
if (filter !== "all" && log.type !== filter) return false;
|
||||
|
||||
if (search) {
|
||||
const searchLower = search.toLowerCase();
|
||||
const contentStr = typeof log.content === "string"
|
||||
? log.content.toLowerCase()
|
||||
: JSON.stringify(log.content || "").toLowerCase();
|
||||
|
||||
return contentStr.includes(searchLower);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [logs, filter, search]);
|
||||
const handleScroll = React.useCallback((e: React.UIEvent<HTMLDivElement>) => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
|
||||
const atBottom = scrollHeight - scrollTop - clientHeight < 50;
|
||||
setAutoScroll(atBottom);
|
||||
}, []);
|
||||
const renderLogEntry = (log: LogEntry, index: number) => {
|
||||
let contentDisplay: React.ReactNode;
|
||||
|
||||
if (typeof log.content === "string") {
|
||||
contentDisplay = log.content;
|
||||
} else if (Array.isArray(log.content)) {
|
||||
contentDisplay = log.content.map((item, i) => (
|
||||
<span key={i} className="console-viewer-arg">
|
||||
{typeof item === "object" && item !== null
|
||||
? JSON.stringify(item, null, 2)
|
||||
: String(item ?? "")}
|
||||
</span>
|
||||
));
|
||||
} else if (typeof log.content === "object" && log.content !== null) {
|
||||
try {
|
||||
contentDisplay = JSON.stringify(log.content, null, 2);
|
||||
} catch {
|
||||
contentDisplay = "[Object]";
|
||||
}
|
||||
} else {
|
||||
contentDisplay = String(log.content ?? "");
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={index} className={`console-viewer-log-entry console-viewer-log-${log.type}`}>
|
||||
<div className="console-viewer-log-time">
|
||||
{timeFormat.format(log.timestamp)}
|
||||
</div>
|
||||
<div className="console-viewer-log-badge">{log.type.toUpperCase()}</div>
|
||||
<div className="console-viewer-log-content">{contentDisplay}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const toggleAutoScroll = React.useCallback(() => {
|
||||
setAutoScroll(prev => !prev);
|
||||
}, []);
|
||||
const handleClear = React.useCallback(() => {
|
||||
clearLogs();
|
||||
setLogs([]);
|
||||
}, []);
|
||||
const clearSearch = React.useCallback(() => {
|
||||
setSearch("");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="console-viewer-backdrop"
|
||||
onClick={closeConsoleViewer}
|
||||
/>
|
||||
<div className="console-viewer-modal" ref={modalRef}>
|
||||
<div className="console-viewer-header">
|
||||
<div className="console-viewer-title">Console Viewer</div>
|
||||
<div className="console-viewer-actions">
|
||||
<Button
|
||||
color={Button.Colors.PRIMARY}
|
||||
look={Button.Looks.OUTLINED}
|
||||
size={Button.Sizes.SMALL}
|
||||
onClick={downloadLogs}
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
<Button
|
||||
color={Button.Colors.RED}
|
||||
look={Button.Looks.OUTLINED}
|
||||
size={Button.Sizes.SMALL}
|
||||
onClick={handleClear}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
<button
|
||||
className="console-viewer-close-button"
|
||||
onClick={closeConsoleViewer}
|
||||
aria-label="Close"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="console-viewer-toolbar">
|
||||
<div className="console-viewer-filters">
|
||||
{["all", "log", "info", "warn", "error"].map(type => (
|
||||
<Button
|
||||
key={type}
|
||||
size={Button.Sizes.SMALL}
|
||||
color={filter === type ? Button.Colors.PRIMARY : Button.Colors.TRANSPARENT}
|
||||
onClick={() => setFilter(type)}
|
||||
>
|
||||
{type === "all" ? "All" :
|
||||
type === "log" ? "Log" :
|
||||
type === "info" ? "Info" :
|
||||
type === "warn" ? "Warning" : "Error"}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="console-viewer-search">
|
||||
<input
|
||||
className="console-viewer-search-input"
|
||||
type="text"
|
||||
placeholder="Search logs..."
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
className="console-viewer-search-clear"
|
||||
onClick={clearSearch}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="console-viewer-logs"
|
||||
ref={logContainerRef}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{filteredLogs.length > 0 ? (
|
||||
filteredLogs.map(renderLogEntry)
|
||||
) : (
|
||||
<div className="console-viewer-no-logs">
|
||||
{search || filter !== "all" ? "No logs match your filters" : "No console logs captured yet"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="console-viewer-status">
|
||||
<div className="console-viewer-count">
|
||||
{filteredLogs.length} {filteredLogs.length === 1 ? "log" : "logs"}
|
||||
{(search || filter !== "all") ? " (filtered)" : ""}
|
||||
</div>
|
||||
<div className="console-viewer-auto-scroll">
|
||||
<label className="auto-scroll-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoScroll}
|
||||
onChange={toggleAutoScroll}
|
||||
/>
|
||||
Auto-scroll
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
let modalContainer: HTMLDivElement | null = null;
|
||||
const ReactRender = {
|
||||
canUseCreateRoot() {
|
||||
return typeof ReactDOM.createRoot === "function";
|
||||
},
|
||||
render(element: React.ReactElement, container: HTMLElement) {
|
||||
if (this.canUseCreateRoot()) {
|
||||
try {
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(element);
|
||||
return {
|
||||
unmount: () => {
|
||||
try {
|
||||
root.unmount();
|
||||
} catch (e) {
|
||||
console.error("[Console Viewer] Error unmounting with createRoot:", e);
|
||||
this.fallbackUnmount(container);
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("[Console Viewer] Error using createRoot:", e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const root = ReactDOM.hydrateRoot(container, element);
|
||||
return {
|
||||
unmount: () => root.unmount()
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("[Console Viewer] Error rendering component:", e);
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
fallbackUnmount(container: HTMLElement) {
|
||||
try {
|
||||
if (ReactRender.canUseCreateRoot()) {
|
||||
console.warn("[Console Viewer] Attempting to unmount using root.unmount");
|
||||
} else {
|
||||
console.warn("[Console Viewer] Falling back to container removal for unmounting.");
|
||||
try {
|
||||
container.remove();
|
||||
} catch (e) {
|
||||
console.error("[Console Viewer] Failed to remove container:", e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[Console Viewer] Error in fallback unmount:", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
let currentRenderer: { unmount: () => void; } | null = null;
|
||||
export function openConsoleViewer() {
|
||||
try {
|
||||
if (!modalContainer) {
|
||||
modalContainer = document.createElement("div");
|
||||
modalContainer.className = "console-viewer-root";
|
||||
document.body.appendChild(modalContainer);
|
||||
}
|
||||
currentRenderer = ReactRender.render(
|
||||
<ErrorBoundary fallback={ErrorFallback}>
|
||||
<ConsoleViewerModal />
|
||||
</ErrorBoundary>,
|
||||
modalContainer
|
||||
);
|
||||
document.body.classList.add("console-viewer-open");
|
||||
} catch (error) {
|
||||
console.error("[Console Viewer] Failed to open modal:", error);
|
||||
}
|
||||
}
|
||||
export function closeConsoleViewer() {
|
||||
try {
|
||||
if (currentRenderer) {
|
||||
currentRenderer.unmount();
|
||||
currentRenderer = null;
|
||||
}
|
||||
if (modalContainer) {
|
||||
document.body.removeChild(modalContainer);
|
||||
modalContainer = null;
|
||||
}
|
||||
document.body.classList.remove("console-viewer-open");
|
||||
document.querySelectorAll(".console-viewer-backdrop").forEach(el => el.remove());
|
||||
} catch (error) {
|
||||
console.error("[Console Viewer] Failed to close modal:", error);
|
||||
document.querySelectorAll(".console-viewer-backdrop, .console-viewer-root").forEach(el => el.remove());
|
||||
document.body.classList.remove("console-viewer-open");
|
||||
modalContainer = null;
|
||||
currentRenderer = null;
|
||||
}
|
||||
}
|
88
src/equicordplugins/consoleViewer/index.tsx
Normal file
88
src/equicordplugins/consoleViewer/index.tsx
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import "./styles.css";
|
||||
|
||||
import { EquicordDevs } from "@utils/constants";
|
||||
import { LazyComponent } from "@utils/react";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findByCode } from "@webpack";
|
||||
|
||||
import { ConsoleLogIcon } from "./components/ConsoleIcon";
|
||||
import { openConsoleViewer } from "./components/ConsoleModal";
|
||||
import settings from "./settings";
|
||||
import { startConsoleCapture, stopConsoleCapture } from "./utils/consoleLogger";
|
||||
|
||||
type CommandReturnValue = any;
|
||||
type Argument = any;
|
||||
type CommandContext = any;
|
||||
|
||||
const HeaderBarIcon = LazyComponent(() => {
|
||||
const filter = ".Icon";
|
||||
return findByCode(filter);
|
||||
});
|
||||
|
||||
export default definePlugin({
|
||||
name: "Console Viewer",
|
||||
description: "View and search console logs in a clean UI modal",
|
||||
authors: [EquicordDevs.SteelTech],
|
||||
dependencies: [],
|
||||
|
||||
settings,
|
||||
|
||||
commands: [{
|
||||
name: "console",
|
||||
description: "Open the console viewer",
|
||||
execute: (args: Argument[], ctx: CommandContext) => {
|
||||
openConsoleViewer();
|
||||
return {
|
||||
send: false,
|
||||
result: "Opened console viewer"
|
||||
} as CommandReturnValue;
|
||||
}
|
||||
}],
|
||||
|
||||
patches: [],
|
||||
|
||||
start() {
|
||||
setTimeout(() => {
|
||||
startConsoleCapture();
|
||||
|
||||
if (settings.store?.iconLocation === "toolbar") {
|
||||
this.showToolbarIcon();
|
||||
} else {
|
||||
this.showToolbarIcon();
|
||||
}
|
||||
}, 0);
|
||||
},
|
||||
|
||||
stop() {
|
||||
stopConsoleCapture();
|
||||
this.removeToolbarIcon();
|
||||
},
|
||||
|
||||
showToolbarIcon() {
|
||||
const toolbar = document.querySelector(".toolbar-3_r2xA");
|
||||
if (!toolbar || document.querySelector(".console-viewer-btn")) return;
|
||||
|
||||
const button = document.createElement("div");
|
||||
button.classList.add("console-viewer-btn", "iconWrapper-2awDjA", "clickable-ZD7xvu");
|
||||
button.innerHTML = ConsoleLogIcon();
|
||||
button.addEventListener("click", openConsoleViewer);
|
||||
button.setAttribute("role", "button");
|
||||
button.setAttribute("aria-label", "Console Viewer");
|
||||
button.setAttribute("tabindex", "0");
|
||||
|
||||
toolbar.prepend(button);
|
||||
},
|
||||
|
||||
removeToolbarIcon() {
|
||||
document.querySelector(".console-viewer-btn")?.remove();
|
||||
},
|
||||
|
||||
showChatIcon() { },
|
||||
removeChatIcon() { }
|
||||
});
|
40
src/equicordplugins/consoleViewer/settings.tsx
Normal file
40
src/equicordplugins/consoleViewer/settings.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { definePluginSettings } from "@api/Settings";
|
||||
import { OptionType } from "@utils/types";
|
||||
|
||||
const settings = definePluginSettings({
|
||||
maxLogEntries: {
|
||||
type: OptionType.NUMBER,
|
||||
description: "Maximum number of log entries to keep (0 for unlimited)",
|
||||
default: 1000,
|
||||
},
|
||||
|
||||
iconLocation: {
|
||||
description: "Where to show the Console Log icon",
|
||||
type: OptionType.SELECT,
|
||||
options: [
|
||||
{ label: "Toolbar", value: "toolbar", default: true },
|
||||
{ label: "Chat input", value: "chat" }
|
||||
],
|
||||
restartNeeded: true
|
||||
},
|
||||
|
||||
groupSimilarLogs: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Group similar consecutive logs",
|
||||
default: true
|
||||
},
|
||||
|
||||
preserveLogsBetweenSessions: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Save logs between plugin restarts or Discord sessions",
|
||||
default: false
|
||||
},
|
||||
|
||||
syntaxHighlighting: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Enable syntax highlighting for objects and code",
|
||||
default: true
|
||||
}
|
||||
});
|
||||
|
||||
export default settings;
|
289
src/equicordplugins/consoleViewer/styles.css
Normal file
289
src/equicordplugins/consoleViewer/styles.css
Normal file
|
@ -0,0 +1,289 @@
|
|||
.console-viewer-btn {
|
||||
cursor: pointer;
|
||||
color: var(--interactive-normal);
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.console-viewer-btn:hover {
|
||||
color: var(--interactive-hover);
|
||||
}
|
||||
|
||||
.console-viewer-root {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.console-viewer-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.console-viewer-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0 0 0 / 85%);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.console-viewer-modal {
|
||||
width: 800px;
|
||||
height: 600px;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
background-color: var(--background-primary);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 20px rgb(0 0 0 / 50%);
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10001;
|
||||
animation: console-viewer-slide-in 250ms ease;
|
||||
}
|
||||
|
||||
body.console-viewer-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.console-viewer-close-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--interactive-normal);
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.console-viewer-close-button:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
color: var(--interactive-hover);
|
||||
}
|
||||
|
||||
.console-viewer-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--background-modifier-accent);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.console-viewer-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.console-viewer-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.console-viewer-toolbar {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--background-modifier-accent);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.console-viewer-filters {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.console-viewer-search {
|
||||
width: 200px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.console-viewer-search-input {
|
||||
width: 100%;
|
||||
background-color: var(--background-tertiary);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 10px;
|
||||
color: var(--text-normal);
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.console-viewer-search-input:focus {
|
||||
box-shadow: 0 0 0 1px var(--brand-experiment);
|
||||
}
|
||||
|
||||
.console-viewer-search-clear {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--interactive-normal);
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.console-viewer-search-clear:hover {
|
||||
color: var(--interactive-hover);
|
||||
}
|
||||
|
||||
.console-viewer-logs {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
font-family: Consolas, 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
background-color: var(--background-secondary);
|
||||
}
|
||||
|
||||
.console-viewer-log-entry {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 4px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.console-viewer-log-time {
|
||||
color: var(--text-muted);
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.console-viewer-log-badge {
|
||||
margin-right: 8px;
|
||||
padding: 1px 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
min-width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.console-viewer-log-content {
|
||||
flex: 1;
|
||||
font-family: Consolas, 'Courier New', monospace;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.console-viewer-log-log {
|
||||
background-color: var(--background-tertiary);
|
||||
}
|
||||
|
||||
.console-viewer-log-log .console-viewer-log-badge {
|
||||
background-color: var(--interactive-normal);
|
||||
color: var(--interactive-active);
|
||||
}
|
||||
|
||||
.console-viewer-log-info {
|
||||
background-color: var(--info-warning-background);
|
||||
}
|
||||
|
||||
.console-viewer-log-info .console-viewer-log-badge {
|
||||
background-color: var(--info-positive-foreground);
|
||||
color: var(--info-positive-text);
|
||||
}
|
||||
|
||||
.console-viewer-log-warn {
|
||||
background-color: var(--info-warning-background);
|
||||
}
|
||||
|
||||
.console-viewer-log-warn .console-viewer-log-badge {
|
||||
background-color: var(--info-warning-foreground);
|
||||
color: var(--info-warning-text);
|
||||
}
|
||||
|
||||
.console-viewer-log-error {
|
||||
background-color: var(--info-danger-background);
|
||||
}
|
||||
|
||||
.console-viewer-log-error .console-viewer-log-badge {
|
||||
background-color: var(--info-danger-foreground);
|
||||
color: var(--info-danger-text);
|
||||
}
|
||||
|
||||
.console-viewer-arg {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.console-viewer-no-logs {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.console-viewer-status {
|
||||
padding: 8px;
|
||||
border-top: 1px solid var(--background-modifier-accent);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.console-viewer-count {
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.console-viewer-auto-scroll {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.console-viewer-error {
|
||||
padding: 16px;
|
||||
color: var(--text-danger);
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
background-color: var(--background-primary);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@keyframes console-viewer-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes console-viewer-slide-in {
|
||||
from {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
116
src/equicordplugins/consoleViewer/utils/consoleLogger.ts
Normal file
116
src/equicordplugins/consoleViewer/utils/consoleLogger.ts
Normal file
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2025 Vendicated and contributors
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import settings from "../settings";
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: number;
|
||||
type: "log" | "info" | "warn" | "error";
|
||||
content: any;
|
||||
stack?: string;
|
||||
}
|
||||
|
||||
const logs: LogEntry[] = [];
|
||||
const originalConsole = {
|
||||
log: console.log,
|
||||
info: console.info,
|
||||
warn: console.warn,
|
||||
error: console.error
|
||||
};
|
||||
|
||||
const consoleLogEvent = new Event("console-captured");
|
||||
|
||||
const DEFAULT_MAX_LOG_ENTRIES = 1000;
|
||||
let isInitialized = false;
|
||||
|
||||
export function startConsoleCapture() {
|
||||
isInitialized = true;
|
||||
|
||||
// Wrap console methods
|
||||
console.log = function (...args) {
|
||||
captureLog("log", ...args);
|
||||
return originalConsole.log.apply(console, args);
|
||||
};
|
||||
|
||||
console.info = function (...args) {
|
||||
captureLog("info", ...args);
|
||||
return originalConsole.info.apply(console, args);
|
||||
};
|
||||
|
||||
console.warn = function (...args) {
|
||||
captureLog("warn", ...args);
|
||||
return originalConsole.warn.apply(console, args);
|
||||
};
|
||||
|
||||
console.error = function (...args) {
|
||||
captureLog("error", ...args);
|
||||
return originalConsole.error.apply(console, args);
|
||||
};
|
||||
}
|
||||
|
||||
export function stopConsoleCapture() {
|
||||
console.log = originalConsole.log;
|
||||
console.info = originalConsole.info;
|
||||
console.warn = originalConsole.warn;
|
||||
console.error = originalConsole.error;
|
||||
}
|
||||
|
||||
function getSetting<T>(key: string, defaultValue: T): T {
|
||||
try {
|
||||
if (!isInitialized || !settings.store) return defaultValue;
|
||||
return settings.store[key] ?? defaultValue;
|
||||
} catch (e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
function captureLog(type: "log" | "info" | "warn" | "error", ...args) {
|
||||
// Create log entry
|
||||
const entry: LogEntry = {
|
||||
timestamp: Date.now(),
|
||||
type,
|
||||
content: args.length === 1 ? args[0] : args,
|
||||
};
|
||||
|
||||
if (type === "error" && args[0] instanceof Error) {
|
||||
entry.stack = args[0].stack;
|
||||
}
|
||||
|
||||
logs.push(entry);
|
||||
|
||||
const maxEntries = getSetting("maxLogEntries", DEFAULT_MAX_LOG_ENTRIES);
|
||||
if (maxEntries > 0 && logs.length > maxEntries) {
|
||||
logs.splice(0, logs.length - maxEntries);
|
||||
}
|
||||
|
||||
window.dispatchEvent(consoleLogEvent);
|
||||
}
|
||||
|
||||
export function getCapturedLogs(): LogEntry[] {
|
||||
return logs;
|
||||
}
|
||||
|
||||
export function clearLogs() {
|
||||
logs.length = 0;
|
||||
window.dispatchEvent(consoleLogEvent);
|
||||
}
|
||||
|
||||
export function downloadLogs() {
|
||||
const content = JSON.stringify(logs, null, 2);
|
||||
const blob = new Blob([content], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const filename = `console-logs-${new Date().toISOString().replace(/:/g, "-")}.json`;
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
}, 60000);
|
||||
}
|
Loading…
Add table
Reference in a new issue