From 6aae4fc2b32d3d877427e5d29a57b4cf5cd5ff44 Mon Sep 17 00:00:00 2001 From: Ategon Date: Wed, 8 Jul 2026 02:32:20 -0400 Subject: [PATCH] Add snapshot types --- packages/client/src/components/App.tsx | 5 +- .../client/src/components/CanvasWrapper.tsx | 33 +- .../src/components/Header/HeaderRight.tsx | 137 +++- .../client/src/contexts/CaptureContext.tsx | 666 ++++++++++++++++++ packages/client/src/lib/canvas.ts | 287 +++++++- packages/client/src/lib/keybinds.ts | 33 +- packages/client/src/style.scss | 9 + 7 files changed, 1150 insertions(+), 20 deletions(-) create mode 100644 packages/client/src/contexts/CaptureContext.tsx diff --git a/packages/client/src/components/App.tsx b/packages/client/src/components/App.tsx index 3d36552..238691c 100644 --- a/packages/client/src/components/App.tsx +++ b/packages/client/src/components/App.tsx @@ -24,6 +24,7 @@ import { Chat, ChatContext } from "./Chat/utils"; import { EventInfoModal } from "./EventInfo/EventInfo"; import AudioContext from "@/contexts/AudioContext"; import PanelContext from "@/contexts/PanelContext"; +import { CaptureContext } from "@/contexts/CaptureContext"; console.log("Client init with version " + __COMMIT_HASH__); @@ -82,7 +83,9 @@ const App = () => { - + + + diff --git a/packages/client/src/components/CanvasWrapper.tsx b/packages/client/src/components/CanvasWrapper.tsx index 8e50307..56d982c 100644 --- a/packages/client/src/components/CanvasWrapper.tsx +++ b/packages/client/src/components/CanvasWrapper.tsx @@ -22,6 +22,7 @@ import { GridOverlay } from "./Overlay/GridOverlay"; import { PaletteLib } from "../lib/palette"; import { getRenderer } from "../lib/utils"; import { usePanel } from "@/contexts/PanelContext"; +import { useCaptureContext } from "@/contexts/CaptureContext"; export const CanvasWrapper = () => { const hasMod = useHasRole("MOD"); @@ -121,6 +122,12 @@ const CanvasInner = () => { enable: templateEnable, } = useTemplateContext(); const PanZoom = useContext(RendererContext); + const { + captureCanvas, + captureView, + captureRegion, + captureTemplateArea, + } = useCaptureContext(); /** * Is the canvas coordinate within the bounds of the canvas? @@ -237,17 +244,35 @@ const CanvasInner = () => { console.log("[CanvasWrapper] received canvasReady"); }); - // setup keybind listeners + return () => { + canvas.current?.destroy(); + }; + }, []); + + useEffect(() => { KeybindManager.on("PIXEL_WHOIS", handlePixelWhois); KeybindManager.on("PICK_COLOR", handlePickPixel); + KeybindManager.on("CAPTURE_CANVAS", captureCanvas); + KeybindManager.on("CAPTURE_VIEW", captureView); + KeybindManager.on("CAPTURE_REGION", captureRegion); + KeybindManager.on("CAPTURE_TEMPLATE", captureTemplateArea); return () => { - canvas.current?.destroy(); - KeybindManager.off("PIXEL_WHOIS", handlePixelWhois); KeybindManager.off("PICK_COLOR", handlePickPixel); + KeybindManager.off("CAPTURE_CANVAS", captureCanvas); + KeybindManager.off("CAPTURE_VIEW", captureView); + KeybindManager.off("CAPTURE_REGION", captureRegion); + KeybindManager.off("CAPTURE_TEMPLATE", captureTemplateArea); }; - }, []); + }, [ + handlePixelWhois, + handlePickPixel, + captureCanvas, + captureView, + captureRegion, + captureTemplateArea, + ]); useEffect(() => { canvas.current?.setPanZoom(PanZoom); diff --git a/packages/client/src/components/Header/HeaderRight.tsx b/packages/client/src/components/Header/HeaderRight.tsx index 7782cc2..4c59a7e 100644 --- a/packages/client/src/components/Header/HeaderRight.tsx +++ b/packages/client/src/components/Header/HeaderRight.tsx @@ -3,12 +3,24 @@ import { useAppContext } from "../../contexts/AppContext"; import { User } from "./User"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { ThemeSwitcher } from "./ThemeSwitcher"; -import { faCog, faGear, faHammer } from "@fortawesome/free-solid-svg-icons"; -import React, { lazy } from "react"; +import { + faBorderAll, + faCamera, + faChevronDown, + faCropSimple, + faEye, + faCog, + faGear, + faHammer, + faDrawPolygon, +} from "@fortawesome/free-solid-svg-icons"; +import React, { lazy, useEffect, useRef, useState } from "react"; import { useHasRole } from "../../hooks/useHasRole"; import { useModerator } from "../../Moderator/Moderator"; import { Button } from "../core/Button"; import { usePanel } from "@/contexts/PanelContext"; +import { useCaptureContext } from "@/contexts/CaptureContext"; +import { AnimatePresence, motion } from "framer-motion"; const OpenChatButton = lazy(() => import("../Chat/OpenChatButton")); @@ -21,13 +33,134 @@ const DynamicChat = () => { export const HeaderRight = () => { const { dispatch } = useModerator(); const Panel = usePanel(); + const { + captureCanvas, + captureView, + captureRegion, + captureTemplateArea, + hasTemplateCapture, + } = useCaptureContext(); const hasAdmin = useHasRole("ADMIN"); const hasMod = useHasRole("MOD"); + const captureMenuRef = useRef(null); + const [captureMenuOpen, setCaptureMenuOpen] = useState(false); + + useEffect(() => { + if (!captureMenuOpen) return; + + const handlePointerDown = (event: PointerEvent) => { + if ( + event.target instanceof Node && + captureMenuRef.current?.contains(event.target) + ) { + return; + } + + setCaptureMenuOpen(false); + }; + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") setCaptureMenuOpen(false); + }; + + window.addEventListener("pointerdown", handlePointerDown); + window.addEventListener("keydown", handleKeyDown); + + return () => { + window.removeEventListener("pointerdown", handlePointerDown); + window.removeEventListener("keydown", handleKeyDown); + }; + }, [captureMenuOpen]); + + const handleCaptureCanvas = () => { + setCaptureMenuOpen(false); + captureCanvas(); + }; + + const handleCaptureView = () => { + setCaptureMenuOpen(false); + captureView(); + }; + + const handleCaptureRegion = () => { + setCaptureMenuOpen(false); + captureRegion(); + }; + + const handleCaptureTemplateArea = () => { + setCaptureMenuOpen(false); + captureTemplateArea(); + }; return (
+
+ + + {captureMenuOpen && ( + + + + + {hasTemplateCapture && ( + + )} + + )} + +