///
import { setScreenshotWatermarkVisible } from "../../../utils/screenshot-watermark";
/**
* How long the watermark stays after the last screenshot signal. Cmd+Shift+4
* still needs a region drag in a system overlay the page never hears about,
* so time is the only cue left. A mistaken chord costs a few seconds of faint
* wordmark, nothing more.
*/
export const SCREENSHOT_WATERMARK_LINGER_MS = 10_000;
/**
* Grace after the window regains focus. Cmd+Shift+5 and Win+Shift+S hand
* focus to a capture app; the shot is taken while we are blurred, and focus
* comes back right after, so the mark only has to survive that hand-back.
*/
const REFOCUS_GRACE_MS = 1_500;
export type ScreenshotKeySignal = "show" | "hide" | "keep";
type ScreenshotKeyEventLike = Pick;
const MODIFIER_KEYS = new Set(["Alt", "AltGraph", "Control", "Fn", "Hyper", "Meta", "OS", "Shift", "Super"]);
/**
* Classify a keydown. The OS swallows the final key of its screenshot chords
* (Cmd+Shift+3/4/5 on macOS, Win+Shift+S on Windows), so the modifier prefix
* is the only part the page ever sees; that prefix reveals the watermark. Any
* complete chord that does reach the page is an app shortcut, not a
* screenshot, and hides it again. Print Screen is delivered on Windows and on
* desktops that do not grab it globally, so it counts as a screenshot outright.
*/
export function screenshotKeySignal(event: ScreenshotKeyEventLike): ScreenshotKeySignal {
if (event.key === "PrintScreen" || event.key === "Snapshot") return "show";
if (MODIFIER_KEYS.has(event.key)) {
return event.metaKey && event.shiftKey ? "show" : "keep";
}
return "hide";
}
export function installScreenshotWatermark(target: Window = window): () => void {
let timer: ReturnType | null = null;
let visible = false;
let blurredWhileVisible = false;
const clearTimer = () => {
if (timer !== null) clearTimeout(timer);
timer = null;
};
const hide = () => {
clearTimer();
visible = false;
blurredWhileVisible = false;
setScreenshotWatermarkVisible(false);
};
const hideAfter = (ms: number) => {
clearTimer();
timer = setTimeout(hide, ms);
};
const show = () => {
visible = true;
setScreenshotWatermarkVisible(true);
hideAfter(SCREENSHOT_WATERMARK_LINGER_MS);
};
const onKeyDown = (event: KeyboardEvent) => {
const signal = screenshotKeySignal(event);
if (signal === "show") show();
else if (signal === "hide") hide();
};
const onKeyUp = (event: KeyboardEvent) => {
// Print Screen on Windows often only surfaces as keyup.
if (screenshotKeySignal(event) === "show" && !MODIFIER_KEYS.has(event.key)) show();
};
// Pointer input means the user is back in the app; system capture overlays
// keep their own mouse events.
const onPointer = () => hide();
// A capture app stealing focus is the screenshot in progress: hold the mark
// for as long as it keeps focus, then let it fade once we are back.
const onBlur = () => {
if (!visible) return;
blurredWhileVisible = true;
clearTimer();
};
const onFocus = () => {
if (blurredWhileVisible) hideAfter(REFOCUS_GRACE_MS);
};
target.addEventListener("keydown", onKeyDown, true);
target.addEventListener("keyup", onKeyUp, true);
target.addEventListener("mousedown", onPointer, true);
target.addEventListener("wheel", onPointer, { capture: true, passive: true });
target.addEventListener("blur", onBlur);
target.addEventListener("focus", onFocus);
return () => {
hide();
target.removeEventListener("keydown", onKeyDown, true);
target.removeEventListener("keyup", onKeyUp, true);
target.removeEventListener("mousedown", onPointer, true);
target.removeEventListener("wheel", onPointer, { capture: true } as EventListenerOptions);
target.removeEventListener("blur", onBlur);
target.removeEventListener("focus", onFocus);
};
}