Skip to content

Commit d9493a8

Browse files
Merge pull request #2210 from ManthanNimodiya/feat/extension-streamlined-bar-and-badge
feat(chrome-extension): streamline recording bar into compact draggable badge on recording start
2 parents 32dd2b3 + 4d70061 commit d9493a8

8 files changed

Lines changed: 1197 additions & 542 deletions

File tree

apps/chrome-extension/src/background/service-worker.ts

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ const shouldAutoPipCaptureSource = (source: RecordingCaptureSource) =>
501501
isWindowCaptureSource(source) && !isLikelyBrowserWindow(source);
502502

503503
const isWebcamPreviewEnabled = (settings: ExtensionSettings) =>
504-
settings.webcam.enabled && Boolean(settings.webcam.deviceId);
504+
Boolean(settings.webcam.enabled);
505505

506506
const shouldShowWebcamPreview = async (
507507
settings: ExtensionSettings,
@@ -817,6 +817,29 @@ const broadcastOverlayHide = async () => {
817817
pendingPreviewTabId = null;
818818
};
819819

820+
const broadcastOverlayCountdown = async (
821+
seconds: number,
822+
durationMs: number,
823+
) => {
824+
const tabs = await getTabs();
825+
await Promise.all(
826+
tabs.map((tab) => {
827+
if (!canInjectIntoTab(tab) || tab.id === undefined) {
828+
return undefined;
829+
}
830+
return sendOverlay(
831+
tab.id,
832+
{
833+
type: "overlay-countdown",
834+
seconds,
835+
durationMs,
836+
},
837+
false,
838+
).catch(() => undefined);
839+
}),
840+
);
841+
};
842+
820843
const broadcastRecordingStatusToTabs = async (status: RecordingStatus) => {
821844
const message: RecordingStatusBroadcast = {
822845
target: "recording-status",
@@ -1621,6 +1644,15 @@ const handleRequest = async (
16211644
: { ok: false, error: response.error };
16221645
}
16231646

1647+
if (message.type === "toggle-microphone-mute") {
1648+
const response = await sendOffscreen({
1649+
target: "offscreen",
1650+
type: "toggle-microphone-mute",
1651+
muted: message.muted,
1652+
});
1653+
return response;
1654+
}
1655+
16241656
if (message.type === "open-options") {
16251657
chrome.tabs.create({ url: chrome.runtime.getURL("options.html") });
16261658
return { ok: true };
@@ -1737,17 +1769,7 @@ const handleRequest = async (
17371769
}
17381770

17391771
if (message.type === "show-countdown") {
1740-
// Relay the offscreen recorder's countdown to the recorded tab. Inject
1741-
// the overlay if it is not there yet; a tab that cannot host it (e.g. a
1742-
// chrome:// page) just shows nothing while the recorder waits out the
1743-
// same countdown, so the count is still kept out of the capture.
1744-
if (message.tabId !== undefined) {
1745-
void sendOverlay(message.tabId, {
1746-
type: "overlay-countdown",
1747-
seconds: message.seconds,
1748-
durationMs: message.durationMs,
1749-
}).catch(() => undefined);
1750-
}
1772+
await broadcastOverlayCountdown(message.seconds, message.durationMs);
17511773
return { ok: true };
17521774
}
17531775

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { useEffect, useRef, useState } from "react";
2+
3+
type HighlightRect = {
4+
top: number;
5+
left: number;
6+
width: number;
7+
height: number;
8+
};
9+
10+
type BlurOverlayProps = {
11+
active: boolean;
12+
onDone: () => void;
13+
};
14+
15+
const OVERLAY_ROOT_ID = "cap-extension-recorder-overlay";
16+
17+
export function BlurOverlay({ active, onDone }: BlurOverlayProps) {
18+
const [highlightRect, setHighlightRect] = useState<HighlightRect | null>(
19+
null,
20+
);
21+
const rafRef = useRef<number | null>(null);
22+
23+
useEffect(() => {
24+
if (!active) {
25+
setHighlightRect(null);
26+
return;
27+
}
28+
29+
const handlePointerMove = (event: PointerEvent) => {
30+
if (rafRef.current !== null) return;
31+
rafRef.current = window.requestAnimationFrame(() => {
32+
rafRef.current = null;
33+
const target = document.elementFromPoint(
34+
event.clientX,
35+
event.clientY,
36+
) as HTMLElement | null;
37+
38+
if (!target || target.closest(`#${OVERLAY_ROOT_ID}`)) {
39+
setHighlightRect(null);
40+
return;
41+
}
42+
43+
const rect = target.getBoundingClientRect();
44+
if (rect.width <= 0 || rect.height <= 0) {
45+
setHighlightRect(null);
46+
return;
47+
}
48+
49+
setHighlightRect({
50+
top: rect.top,
51+
left: rect.left,
52+
width: rect.width,
53+
height: rect.height,
54+
});
55+
});
56+
};
57+
58+
const handleClick = (event: MouseEvent) => {
59+
const target = document.elementFromPoint(
60+
event.clientX,
61+
event.clientY,
62+
) as HTMLElement | null;
63+
64+
if (!target || target.closest(`#${OVERLAY_ROOT_ID}`)) {
65+
return;
66+
}
67+
68+
event.preventDefault();
69+
event.stopPropagation();
70+
71+
if (target.dataset.capBlurred === "true") {
72+
const orig = target.dataset.capOrigFilter ?? "";
73+
if (orig) {
74+
target.style.filter = orig;
75+
} else {
76+
target.style.removeProperty("filter");
77+
}
78+
target.style.removeProperty("user-select");
79+
delete target.dataset.capBlurred;
80+
delete target.dataset.capOrigFilter;
81+
} else {
82+
target.dataset.capOrigFilter = target.style.filter || "";
83+
target.style.setProperty("filter", "blur(12px)", "important");
84+
target.style.setProperty("user-select", "none", "important");
85+
target.dataset.capBlurred = "true";
86+
}
87+
};
88+
89+
const handleKeyDown = (event: KeyboardEvent) => {
90+
if (event.key === "Escape") {
91+
event.preventDefault();
92+
event.stopPropagation();
93+
onDone();
94+
}
95+
};
96+
97+
window.addEventListener("pointermove", handlePointerMove, {
98+
capture: true,
99+
passive: true,
100+
});
101+
window.addEventListener("click", handleClick, {
102+
capture: true,
103+
});
104+
window.addEventListener("keydown", handleKeyDown, { capture: true });
105+
106+
return () => {
107+
if (rafRef.current !== null) {
108+
window.cancelAnimationFrame(rafRef.current);
109+
rafRef.current = null;
110+
}
111+
window.removeEventListener("pointermove", handlePointerMove, {
112+
capture: true,
113+
});
114+
window.removeEventListener("click", handleClick, {
115+
capture: true,
116+
});
117+
window.removeEventListener("keydown", handleKeyDown, { capture: true });
118+
};
119+
}, [active, onDone]);
120+
121+
if (!active || !highlightRect) return null;
122+
123+
return (
124+
<div
125+
className="cap-extension-blur-highlight"
126+
style={{
127+
top: `${highlightRect.top}px`,
128+
left: `${highlightRect.left}px`,
129+
width: `${highlightRect.width}px`,
130+
height: `${highlightRect.height}px`,
131+
}}
132+
aria-hidden
133+
>
134+
<span className="cap-extension-blur-tooltip">Click to blur / unblur</span>
135+
</div>
136+
);
137+
}

0 commit comments

Comments
 (0)