395 lines
18 KiB
JavaScript
395 lines
18 KiB
JavaScript
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { useMitt } from '@/plugins/MittContext';
|
|
import useVideoEditorStore from '@/stores/VideoEditorStore';
|
|
import useMediaStore from '@/stores/MediaStore';
|
|
import { Bold, Italic, Minus, Plus, Type } from 'lucide-react';
|
|
import { useEffect, useState } from 'react';
|
|
|
|
// Import centralized font management
|
|
import { DEFAULT_TEXT_CONFIG, getAvailableFonts, loadFonts } from '@/modules/editor/fonts';
|
|
|
|
export default function TextSidebar({ isOpen, onClose }) {
|
|
const { selectedTextElement } = useVideoEditorStore();
|
|
const { updateCurrentCaption } = useMediaStore();
|
|
const emitter = useMitt();
|
|
const [textValue, setTextValue] = useState('');
|
|
const [fontSize, setFontSize] = useState(24);
|
|
const [isBold, setIsBold] = useState(true);
|
|
const [isItalic, setIsItalic] = useState(false);
|
|
const [fontFamily, setFontFamily] = useState('Montserrat');
|
|
const [fillColor, setFillColor] = useState('#ffffff');
|
|
const [strokeColor, setStrokeColor] = useState('#000000');
|
|
const [strokeWidth, setStrokeWidth] = useState(2);
|
|
|
|
// Font size constraints
|
|
const MIN_FONT_SIZE = 8;
|
|
const MAX_FONT_SIZE = 120;
|
|
const FONT_SIZE_STEP = 2;
|
|
|
|
// Stroke width constraints
|
|
const MIN_STROKE_WIDTH = 0;
|
|
const MAX_STROKE_WIDTH = 3;
|
|
const STROKE_WIDTH_STEP = 1;
|
|
|
|
// Get available fonts from centralized source
|
|
const availableFonts = getAvailableFonts();
|
|
|
|
// Load fonts for preview
|
|
useEffect(() => {
|
|
const loadPreviewFonts = async () => {
|
|
if (fontFamily) {
|
|
try {
|
|
await loadFonts([
|
|
{
|
|
fontFamily,
|
|
fontWeight: isBold ? 700 : 400,
|
|
fontStyle: isItalic ? 'italic' : 'normal',
|
|
fontSize: 16,
|
|
},
|
|
]);
|
|
} catch (error) {
|
|
console.warn('Failed to load preview font:', error);
|
|
}
|
|
}
|
|
};
|
|
loadPreviewFonts();
|
|
}, [fontFamily, isBold, isItalic]);
|
|
|
|
// Update state when selected element changes - THIS KEEPS SIDEBAR IN SYNC WITH TRANSFORMER
|
|
useEffect(() => {
|
|
if (selectedTextElement) {
|
|
setTextValue(selectedTextElement.text || '');
|
|
setFontSize(selectedTextElement.fontSize || DEFAULT_TEXT_CONFIG.fontSize);
|
|
setIsBold(selectedTextElement.fontWeight === 'bold' || selectedTextElement.fontWeight === 700 || true);
|
|
setIsItalic(selectedTextElement.fontStyle === 'italic' || false);
|
|
setFontFamily(selectedTextElement.fontFamily || DEFAULT_TEXT_CONFIG.fontFamily);
|
|
setFillColor(selectedTextElement.fill || DEFAULT_TEXT_CONFIG.fill);
|
|
setStrokeColor(selectedTextElement.stroke || DEFAULT_TEXT_CONFIG.stroke);
|
|
setStrokeWidth(selectedTextElement.strokeWidth || DEFAULT_TEXT_CONFIG.strokeWidth);
|
|
}
|
|
}, [selectedTextElement]);
|
|
|
|
// Handle text changes
|
|
const handleTextChange = (e) => {
|
|
const newText = e.target.value;
|
|
setTextValue(newText);
|
|
|
|
if (selectedTextElement) {
|
|
// Update the timeline element
|
|
emitter.emit('text-update', {
|
|
elementId: selectedTextElement.id,
|
|
updates: { text: newText },
|
|
});
|
|
|
|
// Update MediaStore to maintain single source of truth
|
|
updateCurrentCaption(newText);
|
|
}
|
|
};
|
|
|
|
// Handle font size changes - CLAMP AND UPDATE
|
|
const handleFontSizeChange = (newSize) => {
|
|
const clampedSize = Math.max(MIN_FONT_SIZE, Math.min(MAX_FONT_SIZE, newSize));
|
|
setFontSize(clampedSize);
|
|
|
|
if (selectedTextElement) {
|
|
emitter.emit('text-update', {
|
|
elementId: selectedTextElement.id,
|
|
updates: { fontSize: clampedSize },
|
|
});
|
|
}
|
|
};
|
|
|
|
// Handle stroke width changes
|
|
const handleStrokeWidthChange = (newWidth) => {
|
|
const clampedWidth = Math.max(MIN_STROKE_WIDTH, Math.min(MAX_STROKE_WIDTH, newWidth));
|
|
setStrokeWidth(clampedWidth);
|
|
|
|
if (selectedTextElement) {
|
|
emitter.emit('text-update', {
|
|
elementId: selectedTextElement.id,
|
|
updates: { strokeWidth: clampedWidth },
|
|
});
|
|
}
|
|
};
|
|
|
|
// Handle fill color changes
|
|
const handleFillColorChange = (e) => {
|
|
const newColor = e.target.value;
|
|
setFillColor(newColor);
|
|
|
|
if (selectedTextElement) {
|
|
emitter.emit('text-update', {
|
|
elementId: selectedTextElement.id,
|
|
updates: { fill: newColor },
|
|
});
|
|
}
|
|
};
|
|
|
|
// Handle stroke color changes
|
|
const handleStrokeColorChange = (e) => {
|
|
const newColor = e.target.value;
|
|
setStrokeColor(newColor);
|
|
|
|
if (selectedTextElement) {
|
|
emitter.emit('text-update', {
|
|
elementId: selectedTextElement.id,
|
|
updates: { stroke: newColor },
|
|
});
|
|
}
|
|
};
|
|
|
|
// Handle font family changes
|
|
const handleFontFamilyChange = (newFontFamily) => {
|
|
setFontFamily(newFontFamily);
|
|
|
|
if (selectedTextElement) {
|
|
emitter.emit('text-update', {
|
|
elementId: selectedTextElement.id,
|
|
updates: { fontFamily: newFontFamily },
|
|
});
|
|
}
|
|
};
|
|
|
|
// Handle bold toggle
|
|
const handleBoldToggle = () => {
|
|
const newBoldState = !isBold;
|
|
setIsBold(newBoldState);
|
|
|
|
if (selectedTextElement) {
|
|
emitter.emit('text-update', {
|
|
elementId: selectedTextElement.id,
|
|
updates: { fontWeight: newBoldState ? 'bold' : 'normal' },
|
|
});
|
|
}
|
|
};
|
|
|
|
// Handle italic toggle
|
|
const handleItalicToggle = () => {
|
|
const newItalicState = !isItalic;
|
|
setIsItalic(newItalicState);
|
|
|
|
if (selectedTextElement) {
|
|
emitter.emit('text-update', {
|
|
elementId: selectedTextElement.id,
|
|
updates: { fontStyle: newItalicState ? 'italic' : 'normal' },
|
|
});
|
|
}
|
|
};
|
|
|
|
// Increase font size
|
|
const increaseFontSize = () => {
|
|
handleFontSizeChange(fontSize + FONT_SIZE_STEP);
|
|
};
|
|
|
|
// Decrease font size
|
|
const decreaseFontSize = () => {
|
|
handleFontSizeChange(fontSize - FONT_SIZE_STEP);
|
|
};
|
|
|
|
// Increase stroke width
|
|
const increaseStrokeWidth = () => {
|
|
handleStrokeWidthChange(strokeWidth + STROKE_WIDTH_STEP);
|
|
};
|
|
|
|
// Decrease stroke width
|
|
const decreaseStrokeWidth = () => {
|
|
handleStrokeWidthChange(strokeWidth - STROKE_WIDTH_STEP);
|
|
};
|
|
|
|
return (
|
|
<Sheet open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
|
<SheetContent side="right" className="max-w-[300px] overflow-y-auto dark:bg-neutral-900">
|
|
<SheetHeader>
|
|
<SheetTitle className="flex items-center gap-3">
|
|
<Type className="h-6 w-6" />
|
|
Edit Text
|
|
</SheetTitle>
|
|
</SheetHeader>
|
|
|
|
<div className="mt-6 space-y-4 px-2">
|
|
{selectedTextElement ? (
|
|
<>
|
|
{/* Text Content */}
|
|
<div>
|
|
<label className="text-sm font-medium">Edit your text here</label>
|
|
<Textarea
|
|
value={textValue}
|
|
onChange={handleTextChange}
|
|
placeholder="Enter your text..."
|
|
className="mx-auto mt-2 text-center text-wrap dark:bg-neutral-800"
|
|
rows={4}
|
|
style={{
|
|
maxWidth: 300,
|
|
fontFamily: fontFamily,
|
|
fontSize: `${fontSize * 0.5}px`, // Cap preview size for readability
|
|
fontWeight: isBold ? 'bold' : 'normal',
|
|
fontStyle: isItalic ? 'italic' : 'normal',
|
|
color: fillColor,
|
|
textShadow:
|
|
strokeWidth > 0
|
|
? Array.from({ length: 10 }, (_, i) => {
|
|
const angle = (i * Math.PI * 2) / 8;
|
|
const x = Math.cos(angle) * strokeWidth * 0.6;
|
|
const y = Math.sin(angle) * strokeWidth * 0.6;
|
|
return `${x.toFixed(1)}px ${y.toFixed(1)}px 0 ${strokeColor}`;
|
|
}).join(', ')
|
|
: 'none',
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* Font Family */}
|
|
<div>
|
|
<label className="text-sm font-medium">Font Family</label>
|
|
<Select value={fontFamily} onValueChange={handleFontFamilyChange}>
|
|
<SelectTrigger className="mt-2">
|
|
<SelectValue placeholder="Select font" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{availableFonts.map((font) => (
|
|
<SelectItem key={font.value} value={font.value}>
|
|
<span
|
|
style={{
|
|
fontFamily: font.value,
|
|
fontWeight: isBold ? 'bold' : 'normal',
|
|
fontStyle: isItalic ? 'italic' : 'normal',
|
|
}}
|
|
>
|
|
{font.name}
|
|
</span>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Font Size Controls */}
|
|
<div>
|
|
<label className="text-sm font-medium">Font Size</label>
|
|
<div className="mt-2 flex items-center justify-between rounded-lg border p-2">
|
|
<Button
|
|
variant="default"
|
|
size="icon"
|
|
onClick={decreaseFontSize}
|
|
disabled={fontSize <= MIN_FONT_SIZE}
|
|
className="h-8 w-8"
|
|
>
|
|
<Minus className="h-4 w-4" />
|
|
</Button>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-lg font-semibold">{fontSize}</span>
|
|
<span className="text-sm text-gray-500">px</span>
|
|
</div>
|
|
|
|
<Button
|
|
variant="default"
|
|
size="icon"
|
|
onClick={increaseFontSize}
|
|
disabled={fontSize >= MAX_FONT_SIZE}
|
|
className="h-8 w-8"
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Font Size Range Indicator */}
|
|
<div className="mt-1 text-center text-xs text-gray-500">
|
|
Size range: {MIN_FONT_SIZE}px - {MAX_FONT_SIZE}px
|
|
</div>
|
|
|
|
{/* Note about transformer resize */}
|
|
<div className="mt-1 text-center text-xs text-blue-600">💡 You can also resize by dragging the corners</div>
|
|
</div>
|
|
|
|
{/* Font Style Controls */}
|
|
<div>
|
|
<label className="text-sm font-medium">Font Style</label>
|
|
<div className="mt-2 flex gap-2">
|
|
<Button
|
|
variant={isBold ? 'default' : 'secondary'}
|
|
size=""
|
|
onClick={handleBoldToggle}
|
|
className="flex flex-1 items-center gap-2"
|
|
>
|
|
<Bold className="h-4 w-4" />
|
|
<span className={isBold ? 'font-bold' : 'font-normal'}>Bold</span>
|
|
</Button>
|
|
|
|
<Button
|
|
variant={isItalic ? 'default' : 'secondary'}
|
|
size=""
|
|
onClick={handleItalicToggle}
|
|
className="flex flex-1 items-center gap-2"
|
|
>
|
|
<Italic className="h-4 w-4" />
|
|
<span className={isItalic ? 'italic' : 'not-italic'}>Italic</span>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Font Color */}
|
|
<div>
|
|
<label className="text-sm font-medium">Font Color</label>
|
|
<div className="mt-2 flex items-center gap-2">
|
|
<Input className="h-10 w-20" type="color" value={fillColor} onChange={handleFillColorChange} />
|
|
<Input type="text" value={fillColor} onChange={handleFillColorChange} placeholder="#ffffff" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Outline Color */}
|
|
<div>
|
|
<label className="text-sm font-medium">Outline Color</label>
|
|
<div className="mt-2 flex items-center gap-2">
|
|
<Input className="h-10 w-20" type="color" value={strokeColor} onChange={handleStrokeColorChange} />
|
|
<Input type="text" value={strokeColor} onChange={handleStrokeColorChange} placeholder="#000000" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Outline Thickness */}
|
|
<div>
|
|
<label className="text-sm font-medium">Outline Thickness</label>
|
|
<div className="mt-2 flex items-center justify-between rounded-lg border p-2">
|
|
<Button
|
|
variant="default"
|
|
size="icon"
|
|
onClick={decreaseStrokeWidth}
|
|
disabled={strokeWidth <= MIN_STROKE_WIDTH}
|
|
className="h-8 w-8"
|
|
>
|
|
<Minus className="h-4 w-4" />
|
|
</Button>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-lg font-semibold">{strokeWidth}</span>
|
|
<span className="text-sm text-gray-500">px</span>
|
|
</div>
|
|
|
|
<Button
|
|
variant="default"
|
|
size="icon"
|
|
onClick={increaseStrokeWidth}
|
|
disabled={strokeWidth >= MAX_STROKE_WIDTH}
|
|
className="h-8 w-8"
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className="text-center text-gray-500">
|
|
<Type className="mx-auto h-12 w-12 text-gray-300" />
|
|
<p className="mt-2">Select a text element to edit</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</SheetContent>
|
|
</Sheet>
|
|
);
|
|
}
|