From aea555bbd3ff09e99b6e24ea7d34edf55c0fae09 Mon Sep 17 00:00:00 2001 From: Nurfog Date: Thu, 29 Jan 2026 15:44:43 -0300 Subject: [PATCH] feat: Implement transcription auto-scrolling and language selection, and overhaul MediaPlayer component styling and layout. --- web/studio/src/components/MediaPlayer.tsx | 184 ++++++++++++++-------- 1 file changed, 120 insertions(+), 64 deletions(-) diff --git a/web/studio/src/components/MediaPlayer.tsx b/web/studio/src/components/MediaPlayer.tsx index c97a290..857307f 100644 --- a/web/studio/src/components/MediaPlayer.tsx +++ b/web/studio/src/components/MediaPlayer.tsx @@ -25,9 +25,43 @@ export default function MediaPlayer({ src, type, transcription, locked, onEnded, const [currentCaption, setCurrentCaption] = useState(""); const [language, setLanguage] = useState<"en" | "es">("en"); + const sidebarRef = useRef(null); + const cueRefs = useRef<(HTMLButtonElement | null)[]>([]); + // Hide everything if graded activity or explicitely disabled const shouldShowTranscription = transcription && !locked && !isGraded && showInteractive; + // Determine which cues to use based on language + const getActiveCues = () => { + if (language === "en" && transcription?.cues_en) return transcription.cues_en; + return transcription?.cues || []; + }; + + const activeCues = getActiveCues(); + + // Auto-scroll logic + useEffect(() => { + if (!shouldShowTranscription) return; + + const activeIdx = activeCues.findIndex(cue => + currentTime >= cue.start && currentTime <= cue.end + ); + + if (activeIdx !== -1 && cueRefs.current[activeIdx] && sidebarRef.current) { + const activeElem = cueRefs.current[activeIdx]; + const container = sidebarRef.current; + + // Calculate center position + const offsetTop = activeElem.offsetTop; + const centerScroll = offsetTop - (container.clientHeight / 2) + (activeElem.clientHeight / 2); + + container.scrollTo({ + top: centerScroll, + behavior: 'smooth' + }); + } + }, [currentTime, activeCues, shouldShowTranscription]); + useEffect(() => { const media = type === "video" ? videoRef.current : audioRef.current; if (!media) return; @@ -37,8 +71,12 @@ export default function MediaPlayer({ src, type, transcription, locked, onEnded, setCurrentTime(time); if (onTimeUpdate) onTimeUpdate(time); - if (transcription?.cues) { - const activeCue = transcription.cues.find(cue => + // Re-calculate active cues inside to avoid stale closures in handleTimeUpdate + // or just use the one from the outer scope if dependencies are correct + const cuesToSearch = (language === "en" && transcription?.cues_en) ? transcription.cues_en : (transcription?.cues || []); + + if (cuesToSearch.length > 0) { + const activeCue = cuesToSearch.find(cue => time >= cue.start && time <= cue.end ); setCurrentCaption(activeCue?.text || ""); @@ -55,7 +93,7 @@ export default function MediaPlayer({ src, type, transcription, locked, onEnded, media.removeEventListener("timeupdate", handleTimeUpdate); media.removeEventListener("ended", handleEnded); }; - }, [type, transcription, onEnded]); + }, [type, transcription, onEnded, onTimeUpdate, language]); const handleSeek = (time: number) => { const media = type === "video" ? videoRef.current : audioRef.current; @@ -140,90 +178,108 @@ export default function MediaPlayer({ src, type, transcription, locked, onEnded, }; return ( -
- {/* Top Row: Media + Interactive Sidebar */} -
0 ? 'xl:grid-cols-12' : ''} gap-6 w-full`}> - {/* Media Content */} -
0 ? 'xl:col-span-8' : 'w-full'} relative`}> - {renderMedia()} +
+ {/* Unified Player Unit */} +
+
+ {/* Media Section */} +
+
+ {renderMedia()} +
- {locked && ( -
-
- 🔒 + {locked && ( +
+
+ 🔒 +
+

Playback Limited

+

This exclusive content is protected and can only be viewed once.

+
+ )} +
+ + {/* Integrated Interactive Sidebar */} + {shouldShowTranscription && activeCues.length > 0 && ( +
+
+
+
+

Interactive

+
+
+ + +
+
+
+ {activeCues.map((cue, idx) => ( + + ))}
-

Playback Limited

-

This content can only be played once according to the activity rules.

)}
- - {/* Interactive Sidebar (Cues Only) */} - {shouldShowTranscription && activeCues.length > 0 && ( -
-
-

Interactive Content

-
- - -
-
-
- {activeCues.map((cue, idx) => ( - - ))} -
-
- )}
- {/* Bottom Row: Full Transcription Text */} + {/* Transcription text now clearly separated below the whole unit */} {shouldShowTranscription && ( -
-
-

- 📝 Full Transcription ({language.toUpperCase()}) +
+
+
+

+ 📄 Full Transcription

{!transcription.cues && ( -
+
)}
-
+
"{transcription[language] || "Transcription not available."}" +
+ Official {language.toUpperCase()} Text +
)}
); -} - ); }