Align project captions, fill previews, and protect HUD only during recording

This commit is contained in:
webadderall
2026-09-22 20:19:44 +10:00
parent 3fd3151ac7
commit aa4c53905d
7 changed files with 56 additions and 12 deletions
+1 -2
View File
@@ -404,8 +404,7 @@ export function registerRecordingHandlers(
ipcMain.handle(
"start-native-screen-recording",
async (_, source: SelectedSource, options?: NativeMacRecordingOptions) => {
// Capture starts before the renderer publishes its recording-state
// transition, so protect the HUD at the actual capture boundary.
// Refresh window protection; it applies only while recording is active.
reassertHudOverlayCaptureProtection();
const visibleWindowBounds = source.id?.startsWith("window:")
? await bringSelectedWindowForward(source)
+2 -1
View File
@@ -153,7 +153,8 @@ function applyHudOverlayCaptureProtectionToWindow(hud: BrowserWindow, enabled: b
}
try {
hud.setContentProtection(enabled);
// Keep the idle HUD visible to screenshots and other capture applications.
hud.setContentProtection(enabled && hudOverlayRecordingActive);
} catch (error) {
console.warn("Failed to apply HUD capture protection:", error);
}
+18 -2
View File
@@ -29,7 +29,11 @@ import {
DEFAULT_WALLPAPER_RELATIVE_PATH,
isVideoWallpaperSource,
} from "@/lib/wallpapers";
import { type AspectRatio, formatAspectRatioForCSS } from "@/utils/aspectRatioUtils";
import {
type AspectRatio,
getAspectRatioValue,
formatAspectRatioForCSS,
} from "@/utils/aspectRatioUtils";
import { AnnotationOverlay } from "./AnnotationOverlay";
import { type CaptionEditTarget, normalizeCaptionEditText } from "./captionEditing";
import { buildActiveCaptionLayout } from "./captionLayout";
@@ -218,6 +222,7 @@ function getEffectiveNativeAspectRatio(
interface VideoPlaybackProps {
autoPlay?: boolean;
fillPreview?: boolean;
clipRegions: ClipRegion[];
videoPath: string;
onDurationChange: (duration: number) => void;
@@ -305,6 +310,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
{
videoPath,
autoPlay = false,
fillPreview = false,
onDurationChange,
onPreviewReadyChange,
onTimeUpdate,
@@ -2364,7 +2370,17 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
ref={previewFrameRef}
className="relative overflow-hidden"
style={{
width: "100%",
width: fillPreview
? `max(100cqw, calc(100cqh * ${getAspectRatioValue(aspectRatio, nativeAspectRatio)}))`
: "100%",
...(fillPreview
? {
position: "absolute" as const,
left: "50%",
top: "50%",
transform: "translate(-50%, -50%)",
}
: {}),
aspectRatio: formatAspectRatioForCSS(aspectRatio, nativeAspectRatio),
borderRadius: 0,
clipPath: "none",
@@ -98,9 +98,9 @@ export function ProjectCard({
</span>
)}
</Button>
<div className="flex items-start justify-between gap-3 pt-4">
<AccountAvatar label={accountLabel} className="!size-[46px]" />
<div data-project-caption className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-4 pt-5">
<AccountAvatar label={accountLabel} className="!size-[48px]" />
<div data-project-caption className="h-12 min-w-0 flex-1">
{editing ? (
<form
onSubmit={(event) => {
@@ -111,7 +111,7 @@ export function ProjectCard({
<input
autoFocus
aria-label="Project name"
className="inline-project-name h-6 w-full text-[12px] font-medium"
className="inline-project-name h-5 w-full text-[12px] font-medium"
value={name}
disabled={busy}
maxLength={120}
@@ -128,7 +128,7 @@ export function ProjectCard({
) : (
<p
title={entry.name}
className="truncate text-[12px] font-medium leading-[18px]"
className="truncate text-[12px] font-medium leading-5"
>
{entry.name}
</p>
@@ -81,6 +81,7 @@ export function ProjectHoverPreview({
}, [onFinish]);
return (
<div
style={{ containerType: "size" }}
data-project-hover-preview
className={`pointer-events-none absolute inset-0 overflow-hidden rounded-xl ${playing ? "opacity-100" : "opacity-0"}`}
aria-hidden="true"
@@ -88,6 +89,7 @@ export function ProjectHoverPreview({
<VideoPlayback
{...editor}
autoPlay
fillPreview
ref={playback}
videoPath={data.videoUrl}
webcamVideoPath={data.webcamUrl}
@@ -65,7 +65,7 @@ export function ProjectThumbnail({
loading="lazy"
draggable={false}
onError={() => setFailedSource(sourceKey)}
className="h-full w-full object-contain"
className="h-full w-full object-cover"
/>
) : (
<ImageSquare weight="fill" className="size-8 text-muted-foreground/20" />
+27 -1
View File
@@ -317,7 +317,19 @@ test("dashboard supports creation sort, independent folders, shared settings and
card.querySelector("[data-project-caption]")!.getBoundingClientRect().height,
]),
);
for (const [avatar, caption] of heights) expect(avatar).toBe(caption);
for (const [avatar, caption] of heights) {
expect(avatar).toBe(caption);
expect(avatar).toBe(48);
}
const spacing = await home
.locator("[data-project-caption]")
.first()
.evaluate((element) => {
const caption = element.getBoundingClientRect();
const avatar = element.previousElementSibling!.getBoundingClientRect();
return { gap: caption.left - avatar.right, top: caption.top - avatar.top };
});
expect(spacing).toEqual({ gap: 16, top: 0 });
const avatar = home.locator('[aria-label="Local profile"]').first();
const circle = await avatar.evaluate((element) => {
const style = getComputedStyle(element);
@@ -451,6 +463,20 @@ test("project hover plays a muted five-second preview and stops on exit", async
),
)
.toBe(true);
const fill = await preview.evaluate((element) => {
const card = element.getBoundingClientRect();
const frame = element.firstElementChild!.getBoundingClientRect();
return {
card: card.toJSON(),
frame: frame.toJSON(),
covers:
frame.left <= card.left + 1 &&
frame.top <= card.top + 1 &&
frame.right >= card.right - 1 &&
frame.bottom >= card.bottom - 1,
};
});
expect(fill, JSON.stringify(fill)).toMatchObject({ covers: true });
await expect(preview).toHaveCount(0, { timeout: 8000 });
await page.mouse.move(0, 0);
await card.hover();