@@ -10,6 +10,7 @@ import { TrackingTimeline } from './components/TrackingTimeline';
1010import { VideoPlayer } from './components/VideoPlayer' ;
1111import type { VideoPlayerHandle } from './components/VideoPlayer' ;
1212import { VideoUpload } from './components/VideoUpload' ;
13+ import { SubtitleUpload } from './components/SubtitleUpload' ;
1314import { OffsetConfig } from './components/OffsetConfig' ;
1415import { FreeAnnotationForm } from './components/FreeAnnotationForm' ;
1516import { ALL_EVENT_TYPES } from './constants' ;
@@ -24,6 +25,7 @@ function App() {
2425
2526 // Video state
2627 const [ videoSyncConfig , setVideoSyncConfig ] = useState < VideoSyncConfig | null > ( null ) ;
28+ const [ hasSubtitles , setHasSubtitles ] = useState ( false ) ;
2729 const [ isVideoPlaying , setIsVideoPlaying ] = useState ( false ) ;
2830 const [ videoCacheBust , setVideoCacheBust ] = useState ( 0 ) ;
2931 const videoTimeRef = useRef < number > ( 0 ) ;
@@ -64,12 +66,13 @@ function App() {
6466 setViewMode ( 'timeline' ) ;
6567 setScrollToTimestamp ( null ) ;
6668 try {
67- const [ eventsRes , metaRes , replayRes , annotRes , videoSyncRes ] = await Promise . all ( [
69+ const [ eventsRes , metaRes , replayRes , annotRes , videoSyncRes , subsRes ] = await Promise . all ( [
6870 fetch ( `/api/recordings/${ sessionId } /events` ) ,
6971 fetch ( `/api/recordings/${ sessionId } /metadata` ) ,
7072 fetch ( `/api/recordings/${ sessionId } /replay-eq` ) ,
7173 fetch ( `/api/recordings/${ sessionId } /annotations` ) ,
7274 fetch ( `/api/recordings/${ sessionId } /video-sync` ) ,
75+ fetch ( `/api/recordings/${ sessionId } /subtitles` , { method : 'HEAD' } ) ,
7376 ] ) ;
7477
7578 const events : RecordedEvent [ ] = await eventsRes . json ( ) ;
@@ -94,6 +97,7 @@ function App() {
9497 activeSessionId . current = sessionId ;
9598 setAnnotations ( loadedAnnotations ) ;
9699 setVideoSyncConfig ( syncConfig ) ;
100+ setHasSubtitles ( subsRes . ok ) ;
97101 setVideoCacheBust ( Date . now ( ) ) ;
98102 setIsVideoPlaying ( false ) ;
99103 videoTimeRef . current = 0 ;
@@ -110,6 +114,7 @@ function App() {
110114 activeSessionId . current = null ;
111115 setAnnotations ( [ ] ) ;
112116 setVideoSyncConfig ( null ) ;
117+ setHasSubtitles ( false ) ;
113118 setIsVideoPlaying ( false ) ;
114119 videoTimeRef . current = 0 ;
115120 setViewMode ( 'timeline' ) ;
@@ -122,6 +127,7 @@ function App() {
122127 activeSessionId . current = null ;
123128 setAnnotations ( [ ] ) ;
124129 setVideoSyncConfig ( null ) ;
130+ setHasSubtitles ( false ) ;
125131 setIsVideoPlaying ( false ) ;
126132 videoTimeRef . current = 0 ;
127133 setViewMode ( 'timeline' ) ;
@@ -143,6 +149,19 @@ function App() {
143149 setVideoCacheBust ( Date . now ( ) ) ;
144150 } , [ ] ) ;
145151
152+ const handleSubtitleUploadComplete = useCallback ( ( ) => {
153+ setHasSubtitles ( true ) ;
154+ setVideoCacheBust ( Date . now ( ) ) ;
155+ // `<track>` is remounted via React key change; force-activate after new load.
156+ requestAnimationFrame ( ( ) => videoPlayerRef . current ?. showSubtitles ( ) ) ;
157+ } , [ ] ) ;
158+
159+ const handleOpenSessionFolder = useCallback ( ( ) => {
160+ if ( ! activeSessionId . current ) return ;
161+ fetch ( `/api/recordings/${ encodeURIComponent ( activeSessionId . current ) } /open` , { method : 'POST' } )
162+ . catch ( ( ) => { /* best-effort */ } ) ;
163+ } , [ ] ) ;
164+
146165 const handleOffsetChange = useCallback ( async ( newOffset : number ) => {
147166 if ( ! activeSessionId . current || ! videoSyncConfig ) return ;
148167 const updated = { ...videoSyncConfig , videoTimeAtSessionStartSeconds : newOffset } ;
@@ -256,14 +275,25 @@ function App() {
256275 ? `/api/recordings/${ encodeURIComponent ( activeSessionId . current ) } /video?v=${ videoCacheBust } `
257276 : null ;
258277
278+ const subtitlesUrl = activeSessionId . current && hasSubtitles
279+ ? `/api/recordings/${ encodeURIComponent ( activeSessionId . current ) } /subtitles?v=${ videoCacheBust } `
280+ : null ;
281+
259282 return (
260283 < div className = "app" >
261284 < header className = "app-header" >
262285 < h1 > Artemis Extension Session Analyzer</ h1 >
263286 { session && (
264- < button className = "reset-btn" onClick = { handleBack } >
265- ← Back
266- </ button >
287+ < div className = "header-actions" >
288+ { activeSessionId . current && (
289+ < button className = "reset-btn" onClick = { handleOpenSessionFolder } title = "Open session folder in Finder" >
290+ Open Folder
291+ </ button >
292+ ) }
293+ < button className = "reset-btn" onClick = { handleBack } >
294+ ← Back
295+ </ button >
296+ </ div >
267297 ) }
268298 </ header >
269299
@@ -292,6 +322,7 @@ function App() {
292322 sessionEndTime = { sessionEndTime }
293323 videoTimeAtSessionStartSeconds = { videoSyncConfig . videoTimeAtSessionStartSeconds }
294324 videoUrl = { videoUrl }
325+ subtitlesUrl = { subtitlesUrl }
295326 videoTimeRef = { videoTimeRef }
296327 onPlayStateChange = { handleVideoPlayStateChange }
297328 />
@@ -305,6 +336,11 @@ function App() {
305336 hasVideo = { true }
306337 onUploadComplete = { handleVideoUploadComplete }
307338 />
339+ < SubtitleUpload
340+ sessionId = { activeSessionId . current }
341+ hasSubtitles = { hasSubtitles }
342+ onUploadComplete = { handleSubtitleUploadComplete }
343+ />
308344 </ div >
309345 </ div >
310346 ) }
0 commit comments