mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 23:35:43 +00:00
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:
@@ -73,7 +73,7 @@ export function MicDeviceRow({
|
||||
onSelect: () => void;
|
||||
}) {
|
||||
const { level } = useAudioLevelMeter({
|
||||
enabled: true,
|
||||
enabled: selected,
|
||||
deviceId: device.deviceId,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -43,7 +43,9 @@
|
||||
"imageUploadError": "JPG, PNG, GIF 또는 WebP 이미지 파일을 업로드해 주세요.",
|
||||
"blurStrength": "블러 강도: {{strength}}",
|
||||
"solidColor": "단색 (검열)",
|
||||
"borderRadius": "테두리 반경"
|
||||
"borderRadius": "테두리 반경",
|
||||
"customFonts": "Custom Fonts",
|
||||
"color": "Color"
|
||||
},
|
||||
|
||||
"fontStyles": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -42,7 +42,9 @@
|
||||
"imageUploadError": "请上传 JPG、PNG、GIF 或 WebP 图片文件。",
|
||||
"blurStrength": "模糊强度: {{strength}}",
|
||||
"solidColor": "纯色 (审查)",
|
||||
"borderRadius": "边框半径"
|
||||
"borderRadius": "边框半径",
|
||||
"customFonts": "Custom Fonts",
|
||||
"color": "Color"
|
||||
},
|
||||
|
||||
"fontStyles": {
|
||||
|
||||
Reference in New Issue
Block a user