Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions src/components/video-editor/AnnotationOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
getBlurOverlayColor,
getMosaicGridOverlayColor,
getNormalizedMosaicBlockSize,
getSolidFillColor,
normalizeBlurType,
} from "@/lib/blurEffects";
import { cn } from "@/lib/utils";
import { getArrowComponent } from "./ArrowSvgs";
Expand All @@ -13,7 +15,6 @@ import {
type BlurData,
DEFAULT_BLUR_BLOCK_SIZE,
DEFAULT_BLUR_DATA,
DEFAULT_BLUR_INTENSITY,
} from "./types";

const FREEHAND_POINT_THRESHOLD = 1;
Expand Down Expand Up @@ -85,11 +86,13 @@ export function AnnotationOverlay({
);
const [livePointerPoint, setLivePointerPoint] = useState<{ x: number; y: number } | null>(null);
const mosaicCanvasRef = useRef<HTMLCanvasElement | null>(null);
const blurType = "mosaic";
const blurType =
annotation.type === "blur" ? normalizeBlurType(annotation.blurData?.type) : "mosaic";
const blurOverlayColor =
annotation.type === "blur" ? getBlurOverlayColor(annotation.blurData) : "";
const mosaicGridOverlayColor =
annotation.type === "blur" ? getMosaicGridOverlayColor(annotation.blurData) : "";
const solidFillColor = annotation.type === "blur" ? getSolidFillColor(annotation.blurData) : "";
const [liveRect, setLiveRect] = useState({
x: committedX,
y: committedY,
Expand All @@ -109,7 +112,7 @@ export function AnnotationOverlay({
const { x, y, width, height } = liveRect;

useEffect(() => {
if (annotation.type !== "blur") {
if (annotation.type !== "blur" || normalizeBlurType(annotation.blurData?.type) !== "mosaic") {
return;
}
void previewFrameVersion;
Expand Down Expand Up @@ -367,10 +370,6 @@ export function AnnotationOverlay({

case "blur": {
const shape = annotation.blurData?.shape ?? "rectangle";
const blurIntensity = Math.max(
1,
Math.round(annotation.blurData?.intensity ?? DEFAULT_BLUR_INTENSITY),
);
const blockSize = Math.max(
1,
Math.round(annotation.blurData?.blockSize ?? DEFAULT_BLUR_BLOCK_SIZE),
Expand Down Expand Up @@ -427,9 +426,7 @@ export function AnnotationOverlay({
className="absolute inset-0"
style={{
...shapeMaskStyle,
backdropFilter: blurType === "mosaic" ? "none" : `blur(${blurIntensity}px)`,
WebkitBackdropFilter: blurType === "mosaic" ? "none" : `blur(${blurIntensity}px)`,
backgroundColor: blurOverlayColor,
backgroundColor: blurType === "solid" ? solidFillColor : blurOverlayColor,
opacity: shouldShowFreehandBlurFill ? 1 : 0,
}}
/>
Expand Down
155 changes: 98 additions & 57 deletions src/components/video-editor/BlurSettingsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Trash2 } from "lucide-react";
import { Copy, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Slider } from "@/components/ui/slider";
import { useScopedT } from "@/contexts/I18nContext";
import { getBlurOverlayColor } from "@/lib/blurEffects";
import { getBlurOverlayColor, withBlurDataPatch } from "@/lib/blurEffects";
import { cn } from "@/lib/utils";
import {
type AnnotationRegion,
type BlurColor,
type BlurData,
type BlurShape,
type BlurType,
DEFAULT_BLUR_BLOCK_SIZE,
DEFAULT_BLUR_DATA,
MAX_BLUR_BLOCK_SIZE,
Expand All @@ -19,17 +20,24 @@ interface BlurSettingsPanelProps {
blurRegion: AnnotationRegion;
onBlurDataChange: (blurData: BlurData) => void;
onBlurDataCommit?: () => void;
onDuplicate?: () => void;
onDelete: () => void;
}

export function BlurSettingsPanel({
blurRegion,
onBlurDataChange,
onBlurDataCommit,
onDuplicate,
onDelete,
}: BlurSettingsPanelProps) {
const t = useScopedT("settings");
const activeType = blurRegion.blurData?.type ?? DEFAULT_BLUR_DATA.type;

const blurTypeOptions: Array<{ value: BlurType; labelKey: string }> = [
{ value: "solid", labelKey: "blurTypeSolid" },
{ value: "mosaic", labelKey: "blurTypeMosaic" },
];
const blurShapeOptions: Array<{ value: BlurShape; labelKey: string }> = [
{ value: "rectangle", labelKey: "blurShapeRectangle" },
{ value: "oval", labelKey: "blurShapeOval" },
Expand All @@ -40,15 +48,47 @@ export function BlurSettingsPanel({
];

return (
<div className="min-w-0 p-4 flex flex-col h-full overflow-y-auto custom-scrollbar">
<div
data-testid="blur-settings-panel"
className="min-w-0 p-4 flex flex-col h-full overflow-y-auto custom-scrollbar"
>
<div className="mb-3">
<div className="mb-4">
<span className="text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-500">
{t("annotation.blurTypeMosaic")}
</span>
<div className="mt-1 text-xl font-semibold text-slate-100">
{t("annotation.typeBlur")}
<label className="text-xs font-medium text-slate-300 mb-2 block">
{t("annotation.blurType")}
</label>
<div className="grid grid-cols-2 gap-2">
{blurTypeOptions.map((option) => {
const isActive = activeType === option.value;
return (
<button
key={option.value}
data-testid={`blur-type-${option.value}`}
onClick={() => {
onBlurDataChange(
withBlurDataPatch(blurRegion.blurData, { type: option.value }),
);
requestAnimationFrame(() => {
onBlurDataCommit?.();
});
}}
className={cn(
"h-10 rounded-lg border flex items-center justify-center gap-2 px-3 transition-all text-xs font-medium",
isActive
? "bg-[#34B27B] border-[#34B27B] text-white"
: "bg-white/5 border-white/10 text-slate-200 hover:bg-white/10 hover:border-white/20",
)}
>
{t(`annotation.${option.labelKey}`)}
</button>
);
})}
</div>
{activeType === "mosaic" && (
<p className="mt-2 text-[11px] leading-snug text-amber-400/90">
{t("annotation.mosaicSecurityWarning")}
</p>
)}
</div>

<div className="grid grid-cols-2 gap-2">
Expand All @@ -59,13 +99,7 @@ export function BlurSettingsPanel({
<button
key={shape.value}
onClick={() => {
const nextBlurData: BlurData = {
...DEFAULT_BLUR_DATA,
...blurRegion.blurData,
type: "mosaic",
shape: shape.value,
};
onBlurDataChange(nextBlurData);
onBlurDataChange(withBlurDataPatch(blurRegion.blurData, { shape: shape.value }));
Comment thread
My-Denia marked this conversation as resolved.
requestAnimationFrame(() => {
onBlurDataCommit?.();
});
Expand Down Expand Up @@ -113,13 +147,9 @@ export function BlurSettingsPanel({
<button
key={option.value}
onClick={() => {
const nextBlurData: BlurData = {
...DEFAULT_BLUR_DATA,
...blurRegion.blurData,
type: "mosaic",
color: option.value,
};
onBlurDataChange(nextBlurData);
onBlurDataChange(
withBlurDataPatch(blurRegion.blurData, { color: option.value }),
);
requestAnimationFrame(() => {
onBlurDataCommit?.();
});
Expand Down Expand Up @@ -150,43 +180,54 @@ export function BlurSettingsPanel({
</div>
</div>

<div className="mt-4 p-3 rounded-lg editor-control-surface">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-medium text-slate-300">
{t("annotation.mosaicBlockSize")}
</span>
<span className="text-[10px] text-slate-400 font-mono">
{Math.round(blurRegion.blurData?.blockSize ?? DEFAULT_BLUR_BLOCK_SIZE)}
px
</span>
{activeType === "mosaic" && (
<div className="mt-4 p-3 rounded-lg editor-control-surface">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-medium text-slate-300">
{t("annotation.mosaicBlockSize")}
</span>
<span className="text-[10px] text-slate-400 font-mono">
{Math.round(blurRegion.blurData?.blockSize ?? DEFAULT_BLUR_BLOCK_SIZE)}
px
</span>
</div>
<Slider
value={[blurRegion.blurData?.blockSize ?? DEFAULT_BLUR_BLOCK_SIZE]}
onValueChange={(values) => {
onBlurDataChange(withBlurDataPatch(blurRegion.blurData, { blockSize: values[0] }));
}}
onValueCommit={() => onBlurDataCommit?.()}
min={MIN_BLUR_BLOCK_SIZE}
max={MAX_BLUR_BLOCK_SIZE}
step={1}
className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3"
/>
</div>
<Slider
value={[blurRegion.blurData?.blockSize ?? DEFAULT_BLUR_BLOCK_SIZE]}
onValueChange={(values) => {
onBlurDataChange({
...DEFAULT_BLUR_DATA,
...blurRegion.blurData,
type: "mosaic",
blockSize: values[0],
});
}}
onValueCommit={() => onBlurDataCommit?.()}
min={MIN_BLUR_BLOCK_SIZE}
max={MAX_BLUR_BLOCK_SIZE}
step={1}
className="w-full [&_[role=slider]]:bg-[#34B27B] [&_[role=slider]]:border-[#34B27B] [&_[role=slider]]:h-3 [&_[role=slider]]:w-3"
/>
</div>
)}

<Button
onClick={onDelete}
variant="destructive"
size="sm"
className="w-full gap-2 bg-red-500/10 text-red-400 border border-red-500/20 hover:bg-red-500/20 hover:border-red-500/30 transition-all mt-4"
>
<Trash2 className="w-4 h-4" />
{t("annotation.deleteAnnotation")}
</Button>
<div className="mt-4 grid grid-cols-2 gap-2">
<Button
data-testid="blur-duplicate-button"
onClick={() => onDuplicate?.()}
variant="outline"
size="sm"
disabled={!onDuplicate}
className="w-full gap-2 bg-white/5 text-slate-200 border border-white/10 hover:bg-white/10 hover:border-white/20 transition-all"
>
<Copy className="w-4 h-4" />
{t("annotation.duplicateAnnotation")}
</Button>

<Button
onClick={onDelete}
variant="destructive"
size="sm"
className="w-full gap-2 bg-red-500/10 text-red-400 border border-red-500/20 hover:bg-red-500/20 hover:border-red-500/30 transition-all"
>
<Trash2 className="w-4 h-4" />
{t("annotation.deleteAnnotation")}
</Button>
</div>
</div>
</div>
);
Expand Down
6 changes: 5 additions & 1 deletion src/components/video-editor/KeyboardShortcutsHelp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ export function KeyboardShortcutsHelp() {

return (
<div className="relative group">
<HelpCircle className="w-4 h-4 text-slate-500 hover:text-[#34B27B] transition-colors cursor-help" />
<HelpCircle
data-testid="keyboard-shortcuts-help"
className="w-4 h-4 text-slate-500 hover:text-[#34B27B] transition-colors cursor-help"
/>

<div className="absolute right-0 top-full mt-2 w-64 bg-[#09090b] border border-white/10 rounded-lg p-3 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 shadow-xl z-50">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-semibold text-slate-200">{t("title")}</span>
<button
type="button"
data-testid="keyboard-shortcuts-config-button"
onClick={openConfig}
title="Customize shortcuts"
className="flex items-center gap-1 text-[10px] text-slate-500 hover:text-[#34B27B] transition-colors"
Expand Down
3 changes: 3 additions & 0 deletions src/components/video-editor/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,9 @@ export function SettingsPanel({
blurRegion={selectedBlur}
onBlurDataChange={(blurData) => onBlurDataChange(selectedBlur.id, blurData)}
onBlurDataCommit={onBlurDataCommit}
onDuplicate={
onAnnotationDuplicate ? () => onAnnotationDuplicate(selectedBlur.id) : undefined
}
onDelete={() => onBlurDelete(selectedBlur.id)}
/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/video-editor/ShortcutsConfigDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export function ShortcutsConfigDialog() {
const isCapturing = captureFor === action;
const hasConflict = conflict?.forAction === action;
return (
<div key={action}>
<div key={action} data-testid={`shortcut-action-${action}`}>
<div className="flex items-center justify-between py-1.5 px-1 border-b border-white/5">
<span className="text-sm text-slate-300">{t(`actions.${action}`)}</span>
<button
Expand Down
26 changes: 12 additions & 14 deletions src/components/video-editor/VideoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import { SettingsPanel } from "./SettingsPanel";
import TimelineEditor from "./timeline/TimelineEditor";
import { buildAutoZoomSuggestions } from "./timeline/zoomSuggestionUtils";
import {
buildDuplicatedAnnotationRegion,
buildPastedAnnotationRegion,
buildPastedZoomRegion,
cloneAnnotationRegion,
Expand Down Expand Up @@ -1749,34 +1750,31 @@ export default function VideoEditor() {

const handleAnnotationDuplicate = useCallback(
(id: string) => {
const sourceType = annotationRegions.find((region) => region.id === id)?.type;
if (!sourceType) return;
const duplicateId = `annotation-${nextAnnotationIdRef.current++}`;
const duplicateZIndex = nextAnnotationZIndexRef.current++;
pushState((prev) => {
const source = prev.annotationRegions.find((region) => region.id === id);
if (!source) return {};

const { annotationSource: _stripCaptionLink, ...sourceWithoutCaptionLink } = source;

const duplicate: AnnotationRegion = {
...sourceWithoutCaptionLink,
id: duplicateId,
zIndex: duplicateZIndex,
position: { x: source.position.x + 4, y: source.position.y + 4 },
size: { ...source.size },
style: { ...source.style },
figureData: source.figureData ? { ...source.figureData } : undefined,
};
const duplicate = buildDuplicatedAnnotationRegion(source, duplicateId, duplicateZIndex);

return { annotationRegions: [...prev.annotationRegions, duplicate] };
});
setSelectedAnnotationId(duplicateId);
if (sourceType === "blur") {
setSelectedBlurId(duplicateId);
setSelectedAnnotationId(null);
} else {
setSelectedAnnotationId(duplicateId);
setSelectedBlurId(null);
}
setSelectedZoomId(null);
setSelectedTrimId(null);
setSelectedSpeedId(null);
setSelectedBlurId(null);
setSelectedHighlightId(null);
},
[pushState],
[annotationRegions, pushState],
);

const handleAnnotationDelete = useCallback(
Expand Down
2 changes: 1 addition & 1 deletion src/components/video-editor/VideoPlayback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2151,7 +2151,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
<AnnotationOverlay
key={
item.kind === "blur"
? `${item.region.id}-${overlaySize.width}-${overlaySize.height}-${item.region.blurData?.type ?? "blur"}-${item.region.blurData?.shape ?? "rectangle"}-${item.region.blurData?.color ?? "white"}-${Math.round(item.region.blurData?.blockSize ?? 0)}-${Math.round(item.region.blurData?.intensity ?? 0)}-${(item.region.blurData?.freehandPoints ?? []).map((p) => `${Math.round(p.x)}_${Math.round(p.y)}`).join("-")}`
? `${item.region.id}-${overlaySize.width}-${overlaySize.height}-${item.region.blurData?.type ?? "solid"}-${item.region.blurData?.shape ?? "rectangle"}-${item.region.blurData?.color ?? "white"}-${Math.round(item.region.blurData?.blockSize ?? 0)}-${Math.round(item.region.blurData?.intensity ?? 0)}-${(item.region.blurData?.freehandPoints ?? []).map((p) => `${Math.round(p.x)}_${Math.round(p.y)}`).join("-")}`
: `${item.region.id}-${overlaySize.width}-${overlaySize.height}`
}
annotation={item.region}
Expand Down
Loading
Loading