Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b3a58b6c2d |
@@ -136,6 +136,7 @@ import { useHandleAppTheme } from "./useHandleAppTheme";
|
|||||||
import { getPreferredLanguage } from "./app-language/language-detector";
|
import { getPreferredLanguage } from "./app-language/language-detector";
|
||||||
import { useAppLangCode } from "./app-language/language-state";
|
import { useAppLangCode } from "./app-language/language-state";
|
||||||
import DebugCanvas, {
|
import DebugCanvas, {
|
||||||
|
ConsoleLogger,
|
||||||
debugRenderer,
|
debugRenderer,
|
||||||
isVisualDebuggerEnabled,
|
isVisualDebuggerEnabled,
|
||||||
loadSavedDebugState,
|
loadSavedDebugState,
|
||||||
@@ -1261,6 +1262,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
ref={debugCanvasRef}
|
ref={debugCanvasRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{isVisualDebuggerEnabled() && <ConsoleLogger />}
|
||||||
</Excalidraw>
|
</Excalidraw>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export const STORAGE_KEYS = {
|
|||||||
LOCAL_STORAGE_COLLAB: "excalidraw-collab",
|
LOCAL_STORAGE_COLLAB: "excalidraw-collab",
|
||||||
LOCAL_STORAGE_THEME: "excalidraw-theme",
|
LOCAL_STORAGE_THEME: "excalidraw-theme",
|
||||||
LOCAL_STORAGE_DEBUG: "excalidraw-debug",
|
LOCAL_STORAGE_DEBUG: "excalidraw-debug",
|
||||||
|
LOCAL_STORAGE_DEBUG_CONSOLE: "excalidraw-debug-console",
|
||||||
VERSION_DATA_STATE: "version-dataState",
|
VERSION_DATA_STATE: "version-dataState",
|
||||||
VERSION_FILES: "version-files",
|
VERSION_FILES: "version-files",
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import {
|
|||||||
eyeIcon,
|
eyeIcon,
|
||||||
} from "@excalidraw/excalidraw/components/icons";
|
} from "@excalidraw/excalidraw/components/icons";
|
||||||
import { MainMenu } from "@excalidraw/excalidraw/index";
|
import { MainMenu } from "@excalidraw/excalidraw/index";
|
||||||
import React from "react";
|
import DropdownMenuItemCheckbox from "@excalidraw/excalidraw/components/dropdownMenu/DropdownMenuItemCheckbox";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import { isDevEnv } from "@excalidraw/common";
|
import { isDevEnv } from "@excalidraw/common";
|
||||||
|
|
||||||
@@ -13,7 +14,29 @@ import type { Theme } from "@excalidraw/element/types";
|
|||||||
import { LanguageList } from "../app-language/LanguageList";
|
import { LanguageList } from "../app-language/LanguageList";
|
||||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||||
|
|
||||||
import { saveDebugState } from "./DebugCanvas";
|
import {
|
||||||
|
isVisualDebuggerEnabled,
|
||||||
|
loadConsoleLoggerState,
|
||||||
|
saveDebugState,
|
||||||
|
setConsoleLoggerEnabled,
|
||||||
|
} from "./DebugCanvas";
|
||||||
|
|
||||||
|
const ConsoleLoggerToggle = () => {
|
||||||
|
const [checked, setChecked] = useState(() => loadConsoleLoggerState());
|
||||||
|
return (
|
||||||
|
<DropdownMenuItemCheckbox
|
||||||
|
checked={checked}
|
||||||
|
onSelect={(event) => {
|
||||||
|
const next = !checked;
|
||||||
|
setChecked(next);
|
||||||
|
setConsoleLoggerEnabled(next);
|
||||||
|
event.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Show console log overlay
|
||||||
|
</DropdownMenuItemCheckbox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const AppMainMenu: React.FC<{
|
export const AppMainMenu: React.FC<{
|
||||||
onCollabDialogOpen: () => any;
|
onCollabDialogOpen: () => any;
|
||||||
@@ -77,7 +100,13 @@ export const AppMainMenu: React.FC<{
|
|||||||
</MainMenu.Item>
|
</MainMenu.Item>
|
||||||
)}
|
)}
|
||||||
<MainMenu.Separator />
|
<MainMenu.Separator />
|
||||||
<MainMenu.DefaultItems.Preferences />
|
<MainMenu.DefaultItems.Preferences
|
||||||
|
additionalItems={
|
||||||
|
isDevEnv() && isVisualDebuggerEnabled() ? (
|
||||||
|
<ConsoleLoggerToggle />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
<MainMenu.DefaultItems.ToggleTheme
|
<MainMenu.DefaultItems.ToggleTheme
|
||||||
allowSystemTheme
|
allowSystemTheme
|
||||||
theme={props.theme}
|
theme={props.theme}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from "@excalidraw/excalidraw/renderer/helpers";
|
} from "@excalidraw/excalidraw/renderer/helpers";
|
||||||
import { type AppState } from "@excalidraw/excalidraw/types";
|
import { type AppState } from "@excalidraw/excalidraw/types";
|
||||||
import { arrayToMap, throttleRAF } from "@excalidraw/common";
|
import { arrayToMap, throttleRAF } from "@excalidraw/common";
|
||||||
import { useCallback } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getGlobalFixedPointForBindableElement,
|
getGlobalFixedPointForBindableElement,
|
||||||
@@ -435,6 +435,34 @@ export const loadSavedDebugState = () => {
|
|||||||
export const isVisualDebuggerEnabled = () =>
|
export const isVisualDebuggerEnabled = () =>
|
||||||
Array.isArray(window.visualDebug?.data);
|
Array.isArray(window.visualDebug?.data);
|
||||||
|
|
||||||
|
export const loadConsoleLoggerState = (): boolean => {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_DEBUG_CONSOLE);
|
||||||
|
if (raw !== null) {
|
||||||
|
return JSON.parse(raw) === true;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveConsoleLoggerState = (enabled: boolean) => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
STORAGE_KEYS.LOCAL_STORAGE_DEBUG_CONSOLE,
|
||||||
|
JSON.stringify(enabled),
|
||||||
|
);
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const CONSOLE_LOGGER_TOGGLE_EVENT = "excalidraw-debug-console-toggle";
|
||||||
|
|
||||||
|
export const setConsoleLoggerEnabled = (enabled: boolean) => {
|
||||||
|
saveConsoleLoggerState(enabled);
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent<boolean>(CONSOLE_LOGGER_TOGGLE_EVENT, { detail: enabled }),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const DebugFooter = ({ onChange }: { onChange: () => void }) => {
|
export const DebugFooter = ({ onChange }: { onChange: () => void }) => {
|
||||||
const moveForward = useCallback(() => {
|
const moveForward = useCallback(() => {
|
||||||
if (
|
if (
|
||||||
@@ -459,6 +487,7 @@ export const DebugFooter = ({ onChange }: { onChange: () => void }) => {
|
|||||||
}, [onChange]);
|
}, [onChange]);
|
||||||
const reset = useCallback(() => {
|
const reset = useCallback(() => {
|
||||||
window.visualDebug!.currentFrame = undefined;
|
window.visualDebug!.currentFrame = undefined;
|
||||||
|
_clearLogsCallback?.();
|
||||||
onChange();
|
onChange();
|
||||||
}, [onChange]);
|
}, [onChange]);
|
||||||
const trashFrames = useCallback(() => {
|
const trashFrames = useCallback(() => {
|
||||||
@@ -466,6 +495,7 @@ export const DebugFooter = ({ onChange }: { onChange: () => void }) => {
|
|||||||
window.visualDebug.currentFrame = undefined;
|
window.visualDebug.currentFrame = undefined;
|
||||||
window.visualDebug.data = [];
|
window.visualDebug.data = [];
|
||||||
}
|
}
|
||||||
|
_clearLogsCallback?.();
|
||||||
onChange();
|
onChange();
|
||||||
}, [onChange]);
|
}, [onChange]);
|
||||||
|
|
||||||
@@ -563,4 +593,181 @@ const DebugCanvas = React.forwardRef<HTMLCanvasElement, DebugCanvasProps>(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
type LogLevel = "log" | "info" | "warn" | "error";
|
||||||
|
|
||||||
|
interface LogEntry {
|
||||||
|
id: number;
|
||||||
|
level: LogLevel;
|
||||||
|
message: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOG_COLORS: Record<LogLevel, string> = {
|
||||||
|
log: "rgba(220,220,220,0.9)",
|
||||||
|
info: "rgba(100,180,255,0.9)",
|
||||||
|
warn: "rgba(255,200,60,0.9)",
|
||||||
|
error: "rgba(255,90,90,0.9)",
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_LOGS = 500;
|
||||||
|
let logIdCounter = 0;
|
||||||
|
let _clearLogsCallback: (() => void) | null = null;
|
||||||
|
|
||||||
|
export const ConsoleLogger = () => {
|
||||||
|
const [enabled, setEnabled] = useState(() => loadConsoleLoggerState());
|
||||||
|
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||||
|
const logsRef = useRef<LogEntry[]>([]);
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const dragState = useRef<{ startY: number; startScrollTop: number } | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const isDragging = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (e: Event) => {
|
||||||
|
setEnabled((e as CustomEvent<boolean>).detail);
|
||||||
|
};
|
||||||
|
window.addEventListener(CONSOLE_LOGGER_TOGGLE_EVENT, handler);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener(CONSOLE_LOGGER_TOGGLE_EVENT, handler);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
_clearLogsCallback = () => {
|
||||||
|
logsRef.current = [];
|
||||||
|
setLogs([]);
|
||||||
|
};
|
||||||
|
return () => {
|
||||||
|
_clearLogsCallback = null;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const originals: Record<LogLevel, (...args: unknown[]) => void> = {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
log: console.log.bind(console),
|
||||||
|
info: console.info.bind(console),
|
||||||
|
warn: console.warn.bind(console),
|
||||||
|
error: console.error.bind(console),
|
||||||
|
};
|
||||||
|
|
||||||
|
const patch = (level: LogLevel) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console[level] = (...args: unknown[]) => {
|
||||||
|
originals[level](...args);
|
||||||
|
const message = args
|
||||||
|
.map((a) =>
|
||||||
|
typeof a === "object" ? JSON.stringify(a, null, 0) : String(a),
|
||||||
|
)
|
||||||
|
.join(" ");
|
||||||
|
const entry: LogEntry = {
|
||||||
|
id: ++logIdCounter,
|
||||||
|
level,
|
||||||
|
message,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
logsRef.current = [...logsRef.current, entry].slice(-MAX_LOGS);
|
||||||
|
setLogs([...logsRef.current]);
|
||||||
|
if (!isDragging.current && scrollRef.current) {
|
||||||
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
(["log", "info", "warn", "error"] as LogLevel[]).forEach(patch);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
(["log", "info", "warn", "error"] as LogLevel[]).forEach((level) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console[level] = originals[level];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onPointerDown = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
|
||||||
|
if (!scrollRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dragState.current = {
|
||||||
|
startY: e.clientY,
|
||||||
|
startScrollTop: scrollRef.current.scrollTop,
|
||||||
|
};
|
||||||
|
scrollRef.current.setPointerCapture(e.pointerId);
|
||||||
|
isDragging.current = true;
|
||||||
|
e.preventDefault();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onPointerMove = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
|
||||||
|
if (!dragState.current || !scrollRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const delta = dragState.current.startY - e.clientY;
|
||||||
|
scrollRef.current.scrollTop = dragState.current.startScrollTop + delta;
|
||||||
|
e.preventDefault();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onPointerUp = useCallback(() => {
|
||||||
|
dragState.current = null;
|
||||||
|
isDragging.current = false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!enabled || logs.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
onPointerDown={onPointerDown}
|
||||||
|
onPointerMove={onPointerMove}
|
||||||
|
onPointerUp={onPointerUp}
|
||||||
|
onPointerCancel={onPointerUp}
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
zIndex: 9999,
|
||||||
|
maxWidth: 420,
|
||||||
|
maxHeight: "60vh",
|
||||||
|
overflowY: "auto",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 2,
|
||||||
|
pointerEvents: "all",
|
||||||
|
cursor: "grab",
|
||||||
|
userSelect: "none",
|
||||||
|
scrollbarWidth: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{logs.map((entry) => (
|
||||||
|
<div
|
||||||
|
key={entry.id}
|
||||||
|
style={{
|
||||||
|
background: "rgba(18,18,20,0.55)",
|
||||||
|
backdropFilter: "blur(8px) saturate(1.4)",
|
||||||
|
WebkitBackdropFilter: "blur(8px) saturate(1.4)",
|
||||||
|
borderLeft: `3px solid ${LOG_COLORS[entry.level]}`,
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: "2px 8px",
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontSize: 11,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
color: LOG_COLORS[entry.level],
|
||||||
|
wordBreak: "break-all",
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
opacity: 0.95,
|
||||||
|
boxShadow: "0 1px 4px rgba(0,0,0,0.35)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ opacity: 0.5, marginRight: 6 }}>
|
||||||
|
{entry.level.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
{entry.message}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default DebugCanvas;
|
export default DebugCanvas;
|
||||||
|
|||||||
Reference in New Issue
Block a user