Skip to content

Commit 1d0b598

Browse files
committed
split video/transcript view, overhaul transcript flow, simplify player
1 parent f07b77c commit 1d0b598

9 files changed

Lines changed: 909 additions & 765 deletions

File tree

package-lock.json

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
},
1919
"devDependencies": {
2020
"@types/node": "^22.14.0",
21+
"@types/react": "^19.2.10",
22+
"@types/react-dom": "^19.2.3",
2123
"@typescript-eslint/eslint-plugin": "^8.54.0",
2224
"@typescript-eslint/parser": "^8.54.0",
2325
"@vitejs/plugin-react": "^5.0.0",

src/App.tsx

Lines changed: 102 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import React, { useState } from 'react';
1+
import React, { useState, useRef, useCallback } from 'react';
22
import { SubtitleSegment } from '@/types';
33
import { useTheme } from '@/contexts/ThemeContext';
44
import { useVideoProcessing } from '@/hooks/useVideoProcessing';
55
import { useSubtitles } from '@/hooks/useSubtitles';
66
import { generateSRTContent } from '@/lib/utils';
77
import { Header } from '@/components/Header';
8-
import { SettingsModal } from '@/components/SettingsModal';
8+
import { MenuSheet } from '@/components/MenuSheet';
99
import { UploadZone, handleFileValidation } from '@/components/UploadZone';
1010
import { VideoPlayer } from '@/components/VideoPlayer';
1111
import { Transcript } from '@/components/Transcript';
@@ -16,8 +16,9 @@ export default function App() {
1616
const [selectedLangCode, setSelectedLangCode] = useState<string>('en');
1717
const [subtitles, setSubtitles] = useState<SubtitleSegment[] | null>(null);
1818
const [videoUrl, setVideoUrl] = useState<string | null>(null);
19-
const [showSettings, setShowSettings] = useState(false);
19+
const [showMenu, setShowMenu] = useState(false);
2020
const [isTranslationMode, setIsTranslationMode] = useState(true);
21+
const srtInputRef = useRef<HTMLInputElement>(null);
2122

2223
const { isDark, toggleTheme } = useTheme();
2324
const { apiKey, setApiKey, isProcessing, error, setError, processVideo } = useVideoProcessing();
@@ -26,10 +27,23 @@ export default function App() {
2627
// Initialize settings on mount
2728
React.useEffect(() => {
2829
if (!apiKey) {
29-
setShowSettings(true);
30+
setShowMenu(true);
3031
}
3132
}, []);
3233

34+
// Scroll to transcript section on mobile when video is loaded
35+
React.useEffect(() => {
36+
if (videoUrl && window.innerWidth < 1024) {
37+
// Small delay to ensure the layout has updated
38+
setTimeout(() => {
39+
const transcriptSection = document.querySelector('[data-transcript-section]');
40+
if (transcriptSection) {
41+
transcriptSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
42+
}
43+
}, 300);
44+
}
45+
}, [videoUrl]);
46+
3347
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
3448
if (e.target.files && e.target.files[0]) {
3549
const selectedFile = e.target.files[0];
@@ -47,7 +61,7 @@ export default function App() {
4761
}
4862
};
4963

50-
const handleProcess = async (enableTranslation: boolean = true) => {
64+
const handleProcess = useCallback(async (enableTranslation: boolean = true) => {
5165
if (!file) return;
5266

5367
setIsTranslationMode(enableTranslation);
@@ -58,7 +72,10 @@ export default function App() {
5872
} catch {
5973
// Error is already handled in the hook
6074
}
61-
};
75+
}, [file, selectedLangCode, processVideo]);
76+
77+
const handleProcessSameLanguage = useCallback(() => handleProcess(false), [handleProcess]);
78+
const handleProcessTranslated = useCallback(() => handleProcess(true), [handleProcess]);
6279

6380
const downloadSRT = () => {
6481
if (!subtitles) return;
@@ -86,82 +103,99 @@ export default function App() {
86103

87104
const handleSaveApiKey = (key: string) => {
88105
setApiKey(key);
89-
if (key) setShowSettings(false);
106+
if (key) setShowMenu(false);
90107
};
91108

92109
const textPrimary = isDark ? 'text-white' : 'text-zinc-900';
93110
const textSecondary = isDark ? 'text-zinc-400' : 'text-zinc-500';
94111

95112
return (
96113
<div className={`min-h-screen flex flex-col items-center p-6 md:p-8 w-full`}>
97-
<Header isDark={isDark} onSettingsClick={() => setShowSettings(true)} onThemeToggle={toggleTheme} />
98-
99-
{/* Hero Text */}
100-
<div className="text-center max-w-4xl mx-auto mb-12 space-y-6">
101-
<h2 className={`text-5xl md:text-6xl font-extrabold tracking-tight ${textPrimary}`}>
102-
Subtitles that speak your language
103-
</h2>
104-
<p className={`text-xl ${textSecondary} max-w-2xl mx-auto leading-relaxed`}>
105-
Generate accurate same-language subtitles or translate to any language. Powered by Gemini AI,
106-
delivering perfectly timed captions in seconds.
107-
</p>
108-
</div>
109-
110-
{/* Main Layout */}
111-
<div className="w-full max-w-4xl transition-all duration-500 ease-in-out">
112-
{/* Video Player & Upload */}
113-
<div className="space-y-6">
114-
{!videoUrl ? (
115-
<UploadZone isDark={isDark} onFileChange={handleFileChange} />
116-
) : (
117-
<VideoPlayer
118-
videoRef={videoRef}
119-
videoUrl={videoUrl}
120-
currentSubtitle={currentSubtitle}
121-
selectedLangCode={selectedLangCode}
122-
isDark={isDark}
123-
subtitles={subtitles}
124-
isProcessing={isProcessing}
125-
onLanguageChange={setSelectedLangCode}
126-
onProcessSameLanguage={() => handleProcess(false)}
127-
onProcessTranslated={() => handleProcess(true)}
128-
onRemove={handleRemoveVideo}
129-
onSRTUpload={handleSRTUpload}
130-
/>
131-
)}
132-
133-
{/* Status Messages */}
134-
<StatusMessages
135-
isProcessing={isProcessing}
136-
error={error}
137-
selectedLangCode={selectedLangCode}
138-
isDark={isDark}
139-
isTranslationMode={isTranslationMode}
140-
/>
141-
</div>
114+
<Header isDark={isDark} onMenuClick={() => setShowMenu(true)} onThemeToggle={toggleTheme} />
115+
116+
{/* Main Content Area */}
117+
{!videoUrl ? (
118+
<>
119+
{/* Hero Text */}
120+
<div className="text-center max-w-4xl mx-auto mb-12 space-y-6">
121+
<h2 className={`text-5xl md:text-6xl font-extrabold tracking-tight ${textPrimary}`}>
122+
Subtitles that speak your language
123+
</h2>
124+
<p className={`text-xl ${textSecondary} max-w-2xl mx-auto leading-relaxed`}>
125+
Generate accurate same-language subtitles or translate to any language. Powered by Gemini AI,
126+
delivering perfectly timed captions in seconds.
127+
</p>
128+
</div>
142129

143-
{/* Transcript (Shows below video when generated) */}
144-
{subtitles && (
145-
<div className="mt-6">
146-
<Transcript
147-
subtitles={subtitles}
148-
selectedLangCode={selectedLangCode}
149-
isDark={isDark}
150-
onSeekTo={seekTo}
151-
onDownloadSRT={downloadSRT}
152-
/>
130+
{/* Upload Zone */}
131+
<div className="w-full max-w-4xl">
132+
<UploadZone isDark={isDark} onFileChange={handleFileChange} />
153133
</div>
154-
)}
155-
</div>
134+
</>
135+
) : (
136+
<div className="w-full max-w-7xl">
137+
{/* Two Column Layout: Video Player + Transcript */}
138+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
139+
{/* Video Player Column (2/3 width) */}
140+
<div className="lg:col-span-2 space-y-6">
141+
<VideoPlayer
142+
videoRef={videoRef}
143+
videoUrl={videoUrl}
144+
currentSubtitle={currentSubtitle}
145+
selectedLangCode={selectedLangCode}
146+
isDark={isDark}
147+
subtitles={subtitles}
148+
isProcessing={isProcessing}
149+
onRemove={handleRemoveVideo}
150+
/>
151+
<StatusMessages
152+
isProcessing={isProcessing}
153+
error={error}
154+
selectedLangCode={selectedLangCode}
155+
isDark={isDark}
156+
isTranslationMode={isTranslationMode}
157+
/>
158+
159+
{/* Hero Text Below Video */}
160+
<div className="space-y-3">
161+
<h3 className={`text-2xl font-bold tracking-tight ${textPrimary}`}>
162+
Subtitles that speak your language
163+
</h3>
164+
<p className={`text-base ${textSecondary} leading-relaxed`}>
165+
Generate accurate same-language subtitles or translate to any language. Powered by Gemini AI,
166+
delivering perfectly timed captions in seconds.
167+
</p>
168+
</div>
169+
</div>
170+
171+
{/* Transcript Panel (1/3 width) - Sticky */}
172+
<div className="lg:col-span-1" data-transcript-section>
173+
<Transcript
174+
subtitles={subtitles}
175+
selectedLangCode={selectedLangCode}
176+
isDark={isDark}
177+
isProcessing={isProcessing}
178+
onSeekTo={seekTo}
179+
onDownloadSRT={downloadSRT}
180+
onProcessSameLanguage={handleProcessSameLanguage}
181+
onProcessTranslated={handleProcessTranslated}
182+
onSRTUpload={handleSRTUpload}
183+
onLanguageChange={setSelectedLangCode}
184+
srtInputRef={srtInputRef}
185+
/>
186+
</div>
187+
</div>
188+
</div>
189+
)}
156190

157-
{/* Settings Modal */}
158-
<SettingsModal
159-
isOpen={showSettings}
191+
{/* Menu Sheet */}
192+
<MenuSheet
193+
isOpen={showMenu}
160194
apiKey={apiKey}
161195
isDark={isDark}
162196
onApiKeyChange={setApiKey}
163197
onSave={() => handleSaveApiKey(apiKey)}
164-
onClose={() => setShowSettings(false)}
198+
onClose={() => setShowMenu(false)}
165199
/>
166200
</div>
167201
);

src/components/Header/index.tsx

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,56 @@
1-
import { Settings, Sun, Moon, Subtitles } from 'lucide-react';
1+
import { Sun, Moon, Subtitles, Settings } from 'lucide-react';
22

33
interface HeaderProps {
44
isDark: boolean;
5-
onSettingsClick: () => void;
5+
onMenuClick: () => void;
66
onThemeToggle: () => void;
77
}
88

9-
export const Header = ({ isDark, onSettingsClick, onThemeToggle }: HeaderProps) => {
9+
export const Header = ({ isDark, onMenuClick, onThemeToggle }: HeaderProps) => {
1010
const textPrimary = isDark ? 'text-white' : 'text-zinc-900';
1111

1212
return (
13-
<header className="w-full max-w-7xl flex items-center justify-between mb-16">
14-
{/* Left: Settings (Icon Only) */}
13+
<header className={`w-full max-w-7xl flex items-center justify-between mb-6 pb-3 border-b ${
14+
isDark
15+
? 'border-zinc-800 shadow-lg shadow-zinc-900/20'
16+
: 'border-zinc-200 shadow-lg shadow-zinc-200/50'
17+
}`}>
18+
{/* Left: Settings Button */}
1519
<button
16-
onClick={onSettingsClick}
17-
className={`p-3 rounded-full transition-all duration-200 ${
20+
onClick={onMenuClick}
21+
className={`p-2 rounded-full transition-all duration-200 ${
1822
isDark
1923
? 'hover:bg-zinc-800 text-zinc-400 hover:text-white'
2024
: 'hover:bg-zinc-100 text-zinc-600 hover:text-zinc-900'
2125
}`}
2226
title="API Settings"
2327
>
24-
<Settings className="w-6 h-6" />
28+
<Settings className="w-5 h-5" />
2529
</button>
2630

2731
{/* Center: Logo */}
2832
<div className="flex items-center gap-3">
2933
<div
30-
className={`p-2.5 rounded-xl ${
34+
className={`p-2 rounded-xl ${
3135
isDark ? 'bg-indigo-500/10 ring-1 ring-indigo-500/20' : 'bg-indigo-50 text-indigo-600'
3236
}`}
3337
>
34-
<Subtitles className={`w-6 h-6 ${isDark ? 'text-indigo-400' : 'text-indigo-600'}`} />
38+
<Subtitles className={`w-5 h-5 ${isDark ? 'text-indigo-400' : 'text-indigo-600'}`} />
3539
</div>
36-
<h1 className={`text-2xl font-bold tracking-tight ${textPrimary}`}>SubLingo</h1>
40+
<h1 className={`text-xl font-bold tracking-tight ${textPrimary}`}>SubLingo</h1>
3741
</div>
3842

3943
{/* Right: Theme Toggle */}
4044
<button
4145
onClick={onThemeToggle}
42-
className={`p-3 rounded-full transition-all duration-200 ${
46+
className={`p-2 rounded-full transition-all duration-200 ${
4347
isDark
4448
? 'hover:bg-zinc-800 text-zinc-400 hover:text-white'
4549
: 'hover:bg-zinc-100 text-zinc-600 hover:text-zinc-900'
4650
}`}
4751
title="Toggle Theme"
4852
>
49-
{isDark ? <Sun className="w-6 h-6" /> : <Moon className="w-6 h-6" />}
53+
{isDark ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
5054
</button>
5155
</header>
5256
);

0 commit comments

Comments
 (0)