diff --git a/client-web/src/modules/stories/presentation/components/CreateStoryModal.tsx b/client-web/src/modules/stories/presentation/components/CreateStoryModal.tsx index 07dcfa5..dd6b4e9 100644 --- a/client-web/src/modules/stories/presentation/components/CreateStoryModal.tsx +++ b/client-web/src/modules/stories/presentation/components/CreateStoryModal.tsx @@ -45,8 +45,13 @@ export function CreateStoryModal({ onClose, onCreated }: CreateStoryModalProps) const [bgColor, setBgColor] = useState('#1e1e2e'); const [isUploading, setIsUploading] = useState(false); const [isMuted, setIsMuted] = useState(false); - const [tool, setTool] = useState<'none' | 'crop' | 'brush' | 'stickers' | 'filters' | 'privacy' | 'text'>('none'); const [privacy, setPrivacy] = useState<'all' | 'contacts' | 'selected'>('all'); + const [startTime, setStartTime] = useState(0); + const [endTime, setEndTime] = useState(0); + const [videoDuration, setVideoDuration] = useState(0); + const [tool, setTool] = useState<'none' | 'crop' | 'brush' | 'stickers' | 'filters' | 'privacy' | 'text' | 'trim'>('none'); + const [paused, setPaused] = useState(false); + const videoRef = useRef(null); const [textObjects, setTextObjects] = useState([]); const [selectedTextId, setSelectedTextId] = useState(null); @@ -89,7 +94,22 @@ export function CreateStoryModal({ onClose, onCreated }: CreateStoryModalProps) const url = URL.createObjectURL(file); setMediaPreview(url); setCroppedPreview(null); - setTool(file.type.startsWith('video/') ? 'none' : 'crop'); + + if (file.type.startsWith('video/')) { + const v = document.createElement('video'); + v.src = url; + v.onloadedmetadata = () => { + setVideoDuration(v.duration); + setEndTime(v.duration); + if (v.duration > 120) { + setTool('trim'); + } else { + setTool('none'); + } + }; + } else { + setTool('crop'); + } }; const removeMedia = () => { @@ -171,27 +191,24 @@ export function CreateStoryModal({ onClose, onCreated }: CreateStoryModalProps) let content = ''; if (isVideo && mediaFile) { - // Handle Video: upload original file const res = await StoryApi.uploadVideoToStory(mediaFile); url = res.url; - // Serialize overlays as metadata - content = JSON.stringify({ - stickers: stickers.map(s => ({ ...s, emoji: s.emoji })), // Ensure emoji is string - textObjects: textObjects.map(t => ({ ...t })) - }); + const meta = { + stickers, + textObjects, + trim: startTime > 0 || endTime < videoDuration ? { start: startTime, end: endTime } : null + }; + content = JSON.stringify(meta); } else { - // Handle Image/Text: bake into canvas const canvas = document.createElement('canvas'); canvas.width = TARGET_W; canvas.height = TARGET_H; const ctx = canvas.getContext('2d'); if (!ctx) throw new Error('Canvas init failed'); - // 1. BG Color ctx.fillStyle = bgColor; ctx.fillRect(0, 0, TARGET_W, TARGET_H); - // 2. Base Image Layer const baseSrc = croppedPreview || mediaPreview; if (baseSrc && (mediaFile?.type.startsWith('image/') || !mediaFile)) { const img = await loadImage(baseSrc); @@ -206,12 +223,10 @@ export function CreateStoryModal({ onClose, onCreated }: CreateStoryModalProps) ctx.restore(); } - // 3. Brush Layer if (canvasRef.current) { ctx.drawImage(canvasRef.current, 0, 0, TARGET_W, TARGET_H); } - // 4. Overlays ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; @@ -271,10 +286,16 @@ export function CreateStoryModal({ onClose, onCreated }: CreateStoryModalProps)
{mediaFile?.type.startsWith('video/') && ( - + <> + + + )} + +
+ + + + + )} + {!mediaPreview && tool === 'none' && (
diff --git a/client-web/src/modules/stories/presentation/components/StoryViewer.tsx b/client-web/src/modules/stories/presentation/components/StoryViewer.tsx index 8162e0d..73a32f8 100644 --- a/client-web/src/modules/stories/presentation/components/StoryViewer.tsx +++ b/client-web/src/modules/stories/presentation/components/StoryViewer.tsx @@ -78,14 +78,14 @@ export default function StoryViewer({ stories, initialUserIndex, initialStoryInd const metadata = useMemo(() => { if (!currentStory?.content) return null; try { - const data = JSON.parse(currentStory.content); - if (data.stickers || data.textObjects) return data; + return JSON.parse(currentStory.content); } catch { return null; } - return null; }, [currentStory?.content]); + const trim = useMemo(() => metadata?.trim || null, [metadata]); + useEffect(() => { if (currentUser?.user.id === user?.id) { setIsMuted(false); @@ -166,6 +166,10 @@ export default function StoryViewer({ stories, initialUserIndex, initialStoryInd const video = videoRef.current; if (!video || !isVideo) return; + if (trim && Math.abs(video.currentTime - trim.start) > 0.5 && video.currentTime < trim.start) { + video.currentTime = trim.start; + } + if (paused) { video.pause(); } else { @@ -174,12 +178,24 @@ export default function StoryViewer({ stories, initialUserIndex, initialStoryInd const interval = setInterval(() => { if (video.duration) { - setProgress((video.currentTime / video.duration) * 100); + if (trim) { + const start = trim.start || 0; + const end = trim.end || video.duration; + const current = video.currentTime; + + setProgress(Math.min(100, Math.max(0, ((current - start) / (end - start)) * 100))); + + if (current >= end) { + goNext(); + } + } else { + setProgress((video.currentTime / video.duration) * 100); + } } }, TICK); return () => clearInterval(interval); - }, [isVideo, paused, storyIndex, userIndex]); + }, [isVideo, paused, storyIndex, userIndex, trim, goNext]); useEffect(() => { if (!currentStory || currentUser.user.id === user?.id) return;