fix: address CodeRabbit review feedback (round 2)

- Gate audio level meter to selected mic device only (helperComponents)
- Clear webcam deviceId when 'Default' selected (LaunchWindow)
- Wire i18n for 'Custom Fonts' and 'Color' labels (AnnotationTextTab)
- Restore keyboard focus ring on sidebar buttons (EditorSidebar)
- Add visually-hidden DialogTitle for a11y (ExtensionDetailModal)
- Make extension cards keyboard-accessible with role/tabIndex (ExtensionManagerCards)
- Write smoke export reports for GIF exports before closing (editorExportWorkflow)
- Guard GIF export against zero video dimensions (useEditorExport)
- Allow 'Save Again' before requiring loaded video (useEditorExport)
This commit is contained in:
webadderall
2026-04-20 21:52:07 +10:00
parent 5dd5dbccf0
commit 5caafa95d6
13 changed files with 79 additions and 22 deletions
@@ -73,7 +73,7 @@ export function MicDeviceRow({
onSelect: () => void;
}) {
const { level } = useAudioLevelMeter({
enabled: true,
enabled: selected,
deviceId: device.deviceId,
});
+2 -3
View File
@@ -126,9 +126,8 @@ export function LaunchWindow() {
}, [selectedDeviceId, setMicrophoneDeviceId]);
useEffect(() => {
if (selectedVideoDeviceId && selectedVideoDeviceId !== "default") {
setWebcamDeviceId(selectedVideoDeviceId);
}
if (!selectedVideoDeviceId) return;
setWebcamDeviceId(selectedVideoDeviceId === "default" ? undefined : selectedVideoDeviceId);
}, [selectedVideoDeviceId, setWebcamDeviceId]);
useEffect(() => {
@@ -93,7 +93,7 @@ export function AnnotationTextTab({
{customFonts.length > 0 && (
<>
<div className="px-2 py-1.5 text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
Custom Fonts
{t("annotations.customFonts")}
</div>
{customFonts.map((font) => (
<SelectItem
@@ -273,7 +273,7 @@ export function AnnotationTextTab({
<span className="text-xs text-muted-foreground truncate flex-1 text-left">
{annotation.style.backgroundColor === "transparent"
? t("annotations.none")
: "Color"}
: t("annotations.color")}
</span>
<ChevronDown className="h-3 w-3 opacity-50" />
</Button>
@@ -89,7 +89,7 @@ export function EditorSidebar({
type="button"
onClick={() => prefs.setActiveEffectSection(section.id)}
title={section.label}
className="group relative flex h-9 w-9 items-center justify-center rounded-lg outline-none focus:outline-none focus-visible:outline-none"
className="group relative flex h-9 w-9 items-center justify-center rounded-lg outline-none focus-visible:ring-2 focus-visible:ring-[#2563EB]/50 focus-visible:ring-offset-1"
animate={{ opacity: isActive ? 1 : 0.55 }}
transition={{ duration: 0.14 }}
>
@@ -6,7 +6,7 @@ import {
Tag,
} from "@phosphor-icons/react";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { Switch } from "@/components/ui/switch";
import { useScopedT } from "@/contexts/I18nContext";
import { ExtensionIcon } from "../ExtensionIcon";
@@ -41,6 +41,7 @@ export function ExtensionDetailModal({
return (
<Dialog open onOpenChange={(open) => !open && onClose()}>
<DialogContent className="max-w-md bg-editor-panel border-foreground/10 text-foreground p-0 gap-0 overflow-hidden">
<DialogTitle className="sr-only">{name}</DialogTitle>
<div className="p-5 pb-4">
<div className="flex items-start gap-3.5">
<div className="flex-shrink-0 w-12 h-12 rounded-xl bg-gradient-to-br from-[#2563EB]/20 to-[#2563EB]/5 border border-foreground/10 flex items-center justify-center">
@@ -35,8 +35,10 @@ export function InstalledExtensionCard({
return (
<div
role="button"
tabIndex={0}
className={cn(
"flex items-start gap-3 p-3 rounded-xl border transition-colors cursor-pointer",
"flex items-start gap-3 p-3 rounded-xl border transition-colors cursor-pointer focus-visible:ring-2 focus-visible:ring-[#2563EB]/50 focus-visible:ring-offset-1 outline-none",
isError
? "border-red-500/30 bg-red-500/5"
: isActive
@@ -44,6 +46,7 @@ export function InstalledExtensionCard({
: "border-foreground/[0.06] bg-white/[0.02] hover:bg-foreground/[0.04]",
)}
onClick={onClick}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onClick?.(); } }}
>
<div className="flex-shrink-0 w-8 h-8 rounded-lg bg-foreground/5 border border-foreground/10 flex items-center justify-center overflow-hidden">
<ExtensionIcon
@@ -142,8 +145,11 @@ export function MarketplaceCard({
return (
<div
className="flex items-start gap-3 p-3 rounded-xl border border-foreground/[0.06] bg-white/[0.02] hover:bg-foreground/[0.04] transition-colors cursor-pointer"
role="button"
tabIndex={0}
className="flex items-start gap-3 p-3 rounded-xl border border-foreground/[0.06] bg-white/[0.02] hover:bg-foreground/[0.04] transition-colors cursor-pointer focus-visible:ring-2 focus-visible:ring-[#2563EB]/50 focus-visible:ring-offset-1 outline-none"
onClick={onClick}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onClick?.(); } }}
>
<div className="flex-shrink-0 w-8 h-8 rounded-lg bg-gradient-to-br from-white/10 to-white/5 border border-foreground/10 flex items-center justify-center overflow-hidden">
{extension.iconUrl ? (
@@ -183,6 +183,10 @@ export async function runEditorExport({
)
: await window.electronAPI.saveExportedVideo(arrayBuffer, fileName);
const smokeElapsedMs = smokeExportStartedAt !== null
? Math.round(performance.now() - smokeExportStartedAt)
: undefined;
if (saveResult.canceled) {
setPendingExportSave({ arrayBuffer, fileName });
setExportError(
@@ -190,6 +194,15 @@ export async function runEditorExport({
);
toast.info("Save canceled. You can save again without re-exporting.");
keepExportDialogOpen = true;
if (smokeExportConfig.enabled) {
await writeSmokeExportReport(smokeExportConfig.outputPath, {
success: false,
phase: "save",
format: "gif",
elapsedMs: smokeElapsedMs,
error: "Save canceled",
});
}
} else if (saveResult.success && saveResult.path) {
if (smokeExportStartedAt !== null) {
console.log(
@@ -199,6 +212,13 @@ export async function runEditorExport({
showExportSuccessToast(saveResult.path);
setExportedFilePath(saveResult.path);
if (smokeExportConfig.enabled) {
await writeSmokeExportReport(smokeExportConfig.outputPath, {
success: true,
phase: "complete",
format: "gif",
elapsedMs: smokeElapsedMs,
outputPath: saveResult.path,
});
window.close();
return;
}
@@ -206,6 +226,13 @@ export async function runEditorExport({
setExportError(saveResult.message || "Failed to save GIF");
toast.error(saveResult.message || "Failed to save GIF");
if (smokeExportConfig.enabled) {
await writeSmokeExportReport(smokeExportConfig.outputPath, {
success: false,
phase: "save",
format: "gif",
elapsedMs: smokeElapsedMs,
error: saveResult.message || "Failed to save GIF",
});
window.close();
return;
}
@@ -214,6 +241,16 @@ export async function runEditorExport({
setExportError(result.error || "GIF export failed");
toast.error(result.error || "GIF export failed");
if (smokeExportConfig.enabled) {
const smokeElapsedMs = smokeExportStartedAt !== null
? Math.round(performance.now() - smokeExportStartedAt)
: undefined;
await writeSmokeExportReport(smokeExportConfig.outputPath, {
success: false,
phase: "export",
format: "gif",
elapsedMs: smokeElapsedMs,
error: result.error || "GIF export failed",
});
window.close();
return;
}
@@ -127,16 +127,16 @@ export function useEditorExport({
);
const handleOpenExportDropdown = useCallback(() => {
const { videoPath } = getRenderConfig();
if (!videoPath) {
toast.error("No video loaded");
return;
}
if (hasPendingExportSave) {
setShowExportDropdown(true);
setExportError("Save dialog canceled. Click Save Again to save without re-rendering.");
return;
}
const { videoPath } = getRenderConfig();
if (!videoPath) {
toast.error("No video loaded");
return;
}
setShowExportDropdown(true);
setExportProgress(null);
setExportError(null);
@@ -153,8 +153,12 @@ export function useEditorExport({
toast.error("Video not ready");
return;
}
const sourceWidth = video.videoWidth || 1920;
const sourceHeight = video.videoHeight || 1080;
const sourceWidth = video.videoWidth;
const sourceHeight = video.videoHeight;
if (!sourceWidth || !sourceHeight) {
toast.error("Video dimensions not ready. Please wait for the video to load.");
return;
}
const gifDimensions = calculateOutputDimensions(
sourceWidth,
sourceHeight,
+3 -1
View File
@@ -42,7 +42,9 @@
"imageUploadError": "Please upload a JPG, PNG, GIF, or WebP image file.",
"blurStrength": "Blur Strength: {{strength}}",
"solidColor": "Solid Color (Censorship)",
"borderRadius": "Border Radius"
"borderRadius": "Border Radius",
"customFonts": "Custom Fonts",
"color": "Color"
},
"fontStyles": {
+3 -1
View File
@@ -42,7 +42,9 @@
"imageUploadError": "Por favor sube un archivo de imagen JPG, PNG, GIF o WebP.",
"blurStrength": "Fuerza del Desenfoque: {{strength}}",
"solidColor": "Color Sólido (Censura)",
"borderRadius": "Radio del Borde"
"borderRadius": "Radio del Borde",
"customFonts": "Custom Fonts",
"color": "Color"
},
"fontStyles": {
+3 -1
View File
@@ -43,7 +43,9 @@
"imageUploadError": "JPG, PNG, GIF 또는 WebP 이미지 파일을 업로드해 주세요.",
"blurStrength": "블러 강도: {{strength}}",
"solidColor": "단색 (검열)",
"borderRadius": "테두리 반경"
"borderRadius": "테두리 반경",
"customFonts": "Custom Fonts",
"color": "Color"
},
"fontStyles": {
+3 -1
View File
@@ -43,7 +43,9 @@
"imageUploadError": "Upload een JPG-, PNG-, GIF- of WebP-afbeelding.",
"blurStrength": "Vervagingssterkte: {{strength}}",
"solidColor": "Effen Kleur (Censuur)",
"borderRadius": "Hoekradius"
"borderRadius": "Hoekradius",
"customFonts": "Custom Fonts",
"color": "Color"
},
"fontStyles": {
+3 -1
View File
@@ -42,7 +42,9 @@
"imageUploadError": "请上传 JPG、PNG、GIF 或 WebP 图片文件。",
"blurStrength": "模糊强度: {{strength}}",
"solidColor": "纯色 (审查)",
"borderRadius": "边框半径"
"borderRadius": "边框半径",
"customFonts": "Custom Fonts",
"color": "Color"
},
"fontStyles": {