feat: redesign upload workbench layout

This commit is contained in:
Song367 2026-03-21 12:50:58 +08:00
parent 2d6ad73e1b
commit 5234f5b5db
6 changed files with 320 additions and 150 deletions

View File

@ -15,12 +15,30 @@ describe('App bilingual UI', () => {
expect(screen.getByLabelText('switch-ui-language-zh')).toHaveAttribute('aria-pressed', 'true');
expect(screen.getByText('字幕语言')).toBeInTheDocument();
expect(screen.getByText('上传视频')).toBeInTheDocument();
expect(screen.getAllByText('上传视频').length).toBeGreaterThan(0);
fireEvent.click(screen.getByLabelText('switch-ui-language-en'));
expect(screen.getByLabelText('switch-ui-language-en')).toHaveAttribute('aria-pressed', 'true');
expect(screen.getByText('Subtitle Language')).toBeInTheDocument();
expect(screen.getByText('Upload Video')).toBeInTheDocument();
expect(screen.getAllByText('Upload Video').length).toBeGreaterThan(0);
});
it('shows the upload workbench header and keeps the language switcher visible', () => {
render(<App />);
expect(screen.getByRole('banner')).toBeInTheDocument();
expect(screen.getByLabelText('switch-ui-language-zh')).toBeInTheDocument();
expect(screen.getByLabelText('switch-ui-language-en')).toBeInTheDocument();
fireEvent.click(screen.getByLabelText('switch-ui-language-en'));
expect(screen.getByRole('heading', { name: 'Upload & prepare' })).toBeInTheDocument();
expect(
screen.getByText(
'Upload a source video, tune subtitle defaults, and choose dubbing settings before generation.',
),
).toBeInTheDocument();
expect(screen.getByText('Video Translate')).toBeInTheDocument();
});
});

View File

@ -38,51 +38,82 @@ function AppContent() {
setCurrentView('editor');
};
const languageSwitcher = (
<div
aria-label="app-language-switcher"
className="inline-flex rounded-full border border-slate-200/80 bg-white/80 p-1 shadow-sm shadow-slate-200/60 backdrop-blur"
>
<button
type="button"
aria-label="switch-ui-language-zh"
aria-pressed={locale === 'zh'}
onClick={() => setLocale('zh')}
className={`px-3 py-1.5 text-sm rounded-full transition-colors ${
locale === 'zh' ? 'bg-slate-900 text-white' : 'text-slate-600 hover:bg-slate-100'
}`}
>
{m.app.chinese}
</button>
<button
type="button"
aria-label="switch-ui-language-en"
aria-pressed={locale === 'en'}
onClick={() => setLocale('en')}
className={`px-3 py-1.5 text-sm rounded-full transition-colors ${
locale === 'en' ? 'bg-slate-900 text-white' : 'text-slate-600 hover:bg-slate-100'
}`}
>
{m.app.english}
</button>
</div>
);
return (
<div className="min-h-screen bg-gray-50 text-gray-800 font-sans">
<div className="mx-auto max-w-6xl px-8 pt-4">
<div className="flex justify-end">
<div
aria-label="app-language-switcher"
className="inline-flex rounded-full border border-gray-200 bg-white p-1 shadow-sm"
<div className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(14,165,233,0.10),_transparent_24%),radial-gradient(circle_at_top_right,_rgba(16,185,129,0.10),_transparent_22%),linear-gradient(180deg,_#f8fafc_0%,_#eef2ff_52%,_#f8fafc_100%)] text-slate-900 font-sans">
<div className="mx-auto max-w-7xl px-4 py-5 sm:px-6 sm:py-8 lg:px-8">
{currentView === 'upload' ? (
<header
role="banner"
className="mb-8 flex flex-col gap-5 rounded-[28px] border border-white/70 bg-white/75 px-5 py-5 shadow-[0_18px_60px_-32px_rgba(15,23,42,0.35)] backdrop-blur sm:px-6 lg:flex-row lg:items-start lg:justify-between"
>
<button
type="button"
aria-label="switch-ui-language-zh"
aria-pressed={locale === 'zh'}
onClick={() => setLocale('zh')}
className={`px-3 py-1.5 text-sm rounded-full transition-colors ${
locale === 'zh' ? 'bg-gray-900 text-white' : 'text-gray-600 hover:bg-gray-100'
}`}
>
{m.app.chinese}
</button>
<button
type="button"
aria-label="switch-ui-language-en"
aria-pressed={locale === 'en'}
onClick={() => setLocale('en')}
className={`px-3 py-1.5 text-sm rounded-full transition-colors ${
locale === 'en' ? 'bg-gray-900 text-white' : 'text-gray-600 hover:bg-gray-100'
}`}
>
{m.app.english}
</button>
</div>
</div>
<div className="min-w-0 space-y-3">
<div className="flex flex-wrap items-center gap-3 text-sm">
<span className="rounded-full border border-sky-200 bg-sky-50 px-3 py-1 font-medium text-sky-700">
{m.upload.workbenchEyebrow}
</span>
<span className="text-slate-500">{m.app.productName}</span>
</div>
<div className="space-y-2">
<h1 className="text-3xl font-semibold tracking-tight text-slate-950 sm:text-4xl">
{m.upload.workbenchTitle}
</h1>
<p className="max-w-2xl text-sm leading-6 text-slate-600 sm:text-base">
{m.upload.workbenchDescription}
</p>
</div>
</div>
<div className="flex justify-start lg:justify-end">{languageSwitcher}</div>
</header>
) : (
<div className="mb-4 flex justify-end">{languageSwitcher}</div>
)}
</div>
<div className="mx-auto max-w-7xl px-4 pb-8 sm:px-6 lg:px-8">
{currentView === 'upload' ? (
<UploadScreen onUpload={handleVideoUpload} />
) : (
<EditorScreen
videoFile={videoFile}
targetLanguage={targetLanguage}
ttsLanguage={ttsLanguage}
trimRange={trimRange}
initialSubtitleDefaults={subtitleDefaults}
onBack={() => setCurrentView('upload')}
/>
)}
</div>
{currentView === 'upload' ? (
<UploadScreen onUpload={handleVideoUpload} />
) : (
<EditorScreen
videoFile={videoFile}
targetLanguage={targetLanguage}
ttsLanguage={ttsLanguage}
trimRange={trimRange}
initialSubtitleDefaults={subtitleDefaults}
onBack={() => setCurrentView('upload')}
/>
)}
</div>
);
}

View File

@ -56,6 +56,17 @@ describe('UploadScreen', () => {
expect(preview).toHaveStyle({ fontSize: '32px', bottom: '18%' });
});
it('renders upload, mode, subtitle defaults, and language cards inside the responsive workbench', () => {
renderUploadScreen();
expect(screen.getByTestId('upload-workbench')).toBeInTheDocument();
expect(screen.getByTestId('upload-dropzone-card')).toBeInTheDocument();
expect(screen.getByTestId('upload-settings-column')).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'Mode & Workflow' })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'Subtitle Defaults' })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'Language & Dubbing' })).toBeInTheDocument();
});
it('shows a fixed English subtitle language and the supported TTS languages', () => {
renderUploadScreen();
@ -125,4 +136,12 @@ describe('UploadScreen', () => {
expect(screen.queryByRole('button', { name: /close trim/i })).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: /generate translated video/i })).toBeDisabled();
});
it('keeps the upload interaction wired through the redesigned dropzone card', () => {
renderUploadScreen();
expect(screen.getByTestId('upload-dropzone-card')).toContainElement(
screen.getByLabelText(/upload video file/i),
);
});
});

View File

@ -33,6 +33,10 @@ export default function UploadScreen({
const [tempFile, setTempFile] = useState<File | null>(null);
const [subtitleDefaults, setSubtitleDefaults] = useState<SubtitleDefaults>(DEFAULT_SUBTITLE_DEFAULTS);
const fileInputRef = useRef<HTMLInputElement | null>(null);
const activeModeLabel = mode === 'editing' ? m.upload.editingMode : m.upload.simpleMode;
const activeModeDescription = mode === 'editing' ? m.upload.editingModeDesc : m.upload.simpleModeDesc;
const settingsCardClass =
'app-surface rounded-[28px] border border-white/70 px-5 py-5 shadow-[0_18px_60px_-34px_rgba(15,23,42,0.3)] sm:px-6';
const clearPendingUpload = () => {
setTempFile(null);
@ -55,91 +59,145 @@ export default function UploadScreen({
};
return (
<div className="max-w-6xl mx-auto p-8 flex gap-8 h-screen items-center">
{/* Left: Upload Area */}
<div className="flex-1 bg-white rounded-lg shadow-sm border border-gray-200 p-8 flex flex-col items-center justify-center min-h-[400px]">
<div className="w-full h-full border-2 border-dashed border-gray-300 rounded-lg flex flex-col items-center justify-center bg-gray-50 relative">
<input
ref={fileInputRef}
type="file"
aria-label="Upload video file"
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
accept="video/mp4,video/quicktime,video/webm"
onChange={handleFileChange}
/>
<Upload className="w-16 h-16 text-gray-400 mb-4" />
<p className="text-gray-600 mb-6">{m.upload.clickToUpload}</p>
<button className="bg-[#52c41a] hover:bg-[#46a616] text-white px-8 py-3 rounded-md font-medium flex items-center gap-2 transition-colors w-full max-w-md justify-center pointer-events-none">
<Upload className="w-5 h-5" />
{m.upload.uploadVideo}
</button>
</div>
<p className="text-sm text-gray-500 mt-4 w-full text-left">
{m.upload.supportedFormats}
</p>
</div>
{/* Right: Settings */}
<div className="w-[400px] flex flex-col gap-6">
{/* Mode Selection */}
<div className="flex gap-4">
<div
className={`flex-1 p-4 rounded-lg border-2 cursor-pointer transition-colors ${
mode === 'editing' ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:border-blue-200'
}`}
onClick={() => setMode('editing')}
>
<div className="flex items-center gap-2 mb-1">
{mode === 'editing' ? (
<CheckCircle2 className="w-5 h-5 text-blue-500" />
) : (
<Circle className="w-5 h-5 text-gray-300" />
)}
<span className="font-semibold text-gray-800">{m.upload.editingMode}</span>
</div>
<p className="text-xs text-gray-500 ml-7">{m.upload.editingModeDesc}</p>
<div
data-testid="upload-workbench"
className="grid items-start gap-6 lg:gap-8 xl:grid-cols-[minmax(0,1.65fr)_minmax(320px,420px)]"
>
<section
data-testid="upload-dropzone-card"
className="app-surface min-w-0 rounded-[32px] border border-white/70 px-5 py-5 shadow-[0_24px_80px_-40px_rgba(15,23,42,0.35)] sm:px-6 sm:py-6 lg:px-8 lg:py-8"
>
<div className="mb-6 flex flex-col gap-4 sm:mb-8 lg:flex-row lg:items-start lg:justify-between">
<div className="min-w-0 space-y-2">
<h2 className="text-2xl font-semibold tracking-tight text-slate-950 sm:text-3xl">
{m.upload.uploadPanelTitle}
</h2>
<p className="max-w-2xl text-sm leading-6 text-slate-600 sm:text-base">
{m.upload.uploadPanelDescription}
</p>
</div>
<div
className={`flex-1 p-4 rounded-lg border-2 cursor-pointer transition-colors ${
mode === 'simple' ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:border-blue-200'
}`}
onClick={() => setMode('simple')}
>
<div className="flex items-center gap-2 mb-1">
{mode === 'simple' ? (
<CheckCircle2 className="w-5 h-5 text-blue-500" />
) : (
<Circle className="w-5 h-5 text-gray-300" />
)}
<span className="font-semibold text-gray-800">{m.upload.simpleMode}</span>
</div>
<p className="text-xs text-gray-500 ml-7">{m.upload.simpleModeDesc}</p>
<div className="rounded-2xl border border-sky-100 bg-sky-50/80 px-4 py-3 text-left shadow-sm shadow-sky-100/50">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-sky-600">
{m.upload.modeWorkflowTitle}
</p>
<p className="mt-2 text-sm font-semibold text-slate-900">{activeModeLabel}</p>
<p className="mt-1 max-w-xs text-xs leading-5 text-slate-600">{activeModeDescription}</p>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-start justify-between gap-4 mb-4">
<div>
<h3 className="font-semibold text-gray-800 mb-1">{m.upload.subtitleDefaults}</h3>
<p className="text-xs text-gray-500">
{m.upload.subtitleDefaultsDesc}
<div className="relative overflow-hidden rounded-[28px] border border-slate-200/80 bg-[linear-gradient(180deg,rgba(248,250,252,0.95)_0%,rgba(226,232,240,0.88)_100%)] p-3 sm:p-4">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.12),_transparent_38%)]" />
<div className="relative flex min-h-[360px] flex-col items-center justify-center rounded-[24px] border-2 border-dashed border-slate-300 bg-white/70 px-6 py-12 text-center sm:min-h-[420px] lg:min-h-[500px]">
<input
ref={fileInputRef}
type="file"
aria-label="Upload video file"
className="absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0"
accept="video/mp4,video/quicktime,video/webm"
onChange={handleFileChange}
/>
<div className="pointer-events-none relative z-0 flex max-w-xl flex-col items-center">
<div className="mb-6 rounded-full bg-slate-900 p-5 text-white shadow-lg shadow-slate-300/70">
<Upload className="h-10 w-10 sm:h-12 sm:w-12" />
</div>
<h3 className="text-xl font-semibold text-slate-900 sm:text-2xl">{m.upload.uploadVideo}</h3>
<p className="mt-3 max-w-md text-sm leading-6 text-slate-600 sm:text-base">
{m.upload.clickToUpload}
</p>
<button className="mt-8 flex w-full max-w-sm items-center justify-center gap-2 rounded-2xl bg-[#16a34a] px-6 py-3.5 text-sm font-semibold text-white shadow-lg shadow-emerald-200 transition-colors pointer-events-none sm:text-base">
<Upload className="h-5 w-5" />
{m.upload.uploadVideo}
</button>
</div>
</div>
</div>
<div className="mt-5 flex flex-col gap-3 text-sm text-slate-500 md:flex-row md:items-center md:justify-between">
<p>{m.upload.supportedFormats}</p>
<div className="inline-flex items-center rounded-full border border-slate-200 bg-white/80 px-3 py-1.5 font-medium text-slate-700">
{activeModeLabel}
</div>
</div>
</section>
<aside
data-testid="upload-settings-column"
className="min-w-0 space-y-5 xl:sticky xl:top-6"
>
<section className={settingsCardClass}>
<div className="mb-4">
<h2 className="text-lg font-semibold text-slate-950">{m.upload.modeWorkflowTitle}</h2>
<p className="mt-1 text-sm leading-6 text-slate-600">{m.upload.modeWorkflowDescription}</p>
</div>
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-1">
<button
type="button"
aria-pressed={mode === 'editing'}
className={`rounded-2xl border px-4 py-4 text-left transition-all ${
mode === 'editing'
? 'border-sky-400 bg-sky-50 shadow-sm shadow-sky-100'
: 'border-slate-200 bg-white/80 hover:border-sky-200 hover:bg-slate-50'
}`}
onClick={() => setMode('editing')}
>
<div className="flex items-center gap-3">
{mode === 'editing' ? (
<CheckCircle2 className="h-5 w-5 text-sky-600" />
) : (
<Circle className="h-5 w-5 text-slate-300" />
)}
<span className="font-semibold text-slate-900">{m.upload.editingMode}</span>
</div>
<p className="mt-3 text-sm leading-6 text-slate-600">{m.upload.editingModeDesc}</p>
</button>
<button
type="button"
aria-pressed={mode === 'simple'}
className={`rounded-2xl border px-4 py-4 text-left transition-all ${
mode === 'simple'
? 'border-sky-400 bg-sky-50 shadow-sm shadow-sky-100'
: 'border-slate-200 bg-white/80 hover:border-sky-200 hover:bg-slate-50'
}`}
onClick={() => setMode('simple')}
>
<div className="flex items-center gap-3">
{mode === 'simple' ? (
<CheckCircle2 className="h-5 w-5 text-sky-600" />
) : (
<Circle className="h-5 w-5 text-slate-300" />
)}
<span className="font-semibold text-slate-900">{m.upload.simpleMode}</span>
</div>
<p className="mt-3 text-sm leading-6 text-slate-600">{m.upload.simpleModeDesc}</p>
</button>
</div>
</section>
<section className={settingsCardClass}>
<div className="mb-4 flex items-start justify-between gap-4">
<div>
<h2 className="text-lg font-semibold text-slate-950">{m.upload.subtitleDefaults}</h2>
<p className="mt-1 text-sm leading-6 text-slate-600">{m.upload.subtitleDefaultsDesc}</p>
</div>
<button
type="button"
className="text-sm text-blue-600 hover:text-blue-700 font-medium"
className="text-sm font-medium text-sky-700 transition-colors hover:text-sky-800"
onClick={() => setSubtitleDefaults(DEFAULT_SUBTITLE_DEFAULTS)}
>
{m.upload.reset}
</button>
</div>
<div className="rounded-xl border border-gray-200 bg-slate-950 p-4 mb-4">
<div className="relative h-48 rounded-lg overflow-hidden bg-[radial-gradient(circle_at_top,_rgba(148,163,184,0.35),_transparent_40%),linear-gradient(180deg,_#1e293b_0%,_#0f172a_100%)]">
<div className="mb-5 rounded-[24px] border border-slate-200 bg-slate-950/95 p-3">
<div className="relative h-40 overflow-hidden rounded-[18px] bg-[radial-gradient(circle_at_top,_rgba(148,163,184,0.35),_transparent_40%),linear-gradient(180deg,_#1e293b_0%,_#0f172a_100%)] sm:h-44">
<div className="absolute inset-0 bg-[linear-gradient(90deg,transparent_0%,rgba(255,255,255,0.04)_50%,transparent_100%)]" />
<p
data-testid="upload-subtitle-preview"
className="absolute left-1/2 -translate-x-1/2 text-white font-semibold text-center whitespace-pre-wrap max-w-[88%] px-3 py-1"
className="absolute left-1/2 max-w-[88%] -translate-x-1/2 whitespace-pre-wrap px-3 py-1 text-center font-semibold text-white"
style={{
bottom: `${subtitleDefaults.bottomOffsetPercent}%`,
fontSize: `${subtitleDefaults.fontSize}px`,
@ -153,11 +211,13 @@ export default function UploadScreen({
<div className="space-y-4">
<div>
<div className="flex items-center justify-between mb-2">
<label htmlFor="subtitle-position" className="text-sm font-medium text-gray-700">
<div className="mb-2 flex items-center justify-between">
<label htmlFor="subtitle-position" className="text-sm font-medium text-slate-700">
{m.upload.subtitleInitialPosition}
</label>
<span className="text-xs text-gray-500">{format(m.upload.fromBottom, { value: subtitleDefaults.bottomOffsetPercent })}</span>
<span className="text-xs text-slate-500">
{format(m.upload.fromBottom, { value: subtitleDefaults.bottomOffsetPercent })}
</span>
</div>
<input
id="subtitle-position"
@ -173,16 +233,18 @@ export default function UploadScreen({
bottomOffsetPercent: Number(e.target.value),
}))
}
className="w-full accent-blue-500"
className="w-full accent-sky-500"
/>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<label htmlFor="subtitle-size" className="text-sm font-medium text-gray-700">
<div className="mb-2 flex items-center justify-between">
<label htmlFor="subtitle-size" className="text-sm font-medium text-slate-700">
{m.upload.subtitleInitialSize}
</label>
<span className="text-xs text-gray-500">{format(m.upload.pxValue, { value: subtitleDefaults.fontSize })}</span>
<span className="text-xs text-slate-500">
{format(m.upload.pxValue, { value: subtitleDefaults.fontSize })}
</span>
</div>
<input
id="subtitle-size"
@ -198,48 +260,61 @@ export default function UploadScreen({
fontSize: Number(e.target.value),
}))
}
className="w-full accent-blue-500"
className="w-full accent-sky-500"
/>
</div>
</div>
</div>
</section>
{/* Language Selection */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 flex-1 flex flex-col">
<section className={`${settingsCardClass} flex flex-col`}>
<div className="mb-5">
<h3 className="font-semibold text-gray-800 mb-1">{m.upload.subtitleLanguage}</h3>
<p className="text-xs text-gray-500 mb-3">{m.upload.subtitleLanguageHint}</p>
<div className="inline-flex items-center rounded-md border border-gray-200 bg-gray-50 px-3 py-2 text-sm font-medium text-gray-700">
<h2 className="text-lg font-semibold text-slate-950">{m.upload.languageDubbingTitle}</h2>
<p className="mt-1 text-sm leading-6 text-slate-600">{m.upload.languageDubbingDescription}</p>
</div>
<div className="mb-5 rounded-2xl border border-slate-200 bg-white/80 p-4">
<h3 className="font-semibold text-slate-900">{m.upload.subtitleLanguage}</h3>
<p className="mt-1 text-xs text-slate-500">{m.upload.subtitleLanguageHint}</p>
<div className="mt-3 inline-flex items-center rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-sm font-medium text-slate-700">
{m.upload.languages.en}
</div>
</div>
<h3 className="font-semibold text-gray-800 mb-1">{m.upload.ttsLanguage}</h3>
<p className="text-xs text-gray-500 mb-4">{m.upload.ttsLanguageHint}</p>
{/* Alphabet Tabs */}
<div className="flex gap-4 border-b border-gray-100 pb-2 mb-4 text-sm text-gray-500 overflow-x-auto">
<button className="font-medium text-blue-600 border-b-2 border-blue-600 pb-2 -mb-[9px]">{m.upload.popular}</button>
<button className="hover:text-gray-800">{m.upload.groupABC}</button>
<button className="hover:text-gray-800">{m.upload.groupGHI}</button>
<button className="hover:text-gray-800">{m.upload.groupDEF}</button>
<div className="mb-4">
<h3 className="font-semibold text-slate-900">{m.upload.ttsLanguage}</h3>
<p className="mt-1 text-xs text-slate-500">{m.upload.ttsLanguageHint}</p>
</div>
{/* Language List */}
<div className="flex-1 overflow-y-auto pr-2 custom-scrollbar">
<div className="mb-4 flex gap-4 overflow-x-auto border-b border-slate-200 pb-2 text-sm text-slate-500">
<button type="button" className="border-b-2 border-sky-600 pb-2 font-medium text-sky-600 -mb-[9px]">
{m.upload.popular}
</button>
<button type="button" className="hover:text-slate-800">
{m.upload.groupABC}
</button>
<button type="button" className="hover:text-slate-800">
{m.upload.groupGHI}
</button>
<button type="button" className="hover:text-slate-800">
{m.upload.groupDEF}
</button>
</div>
<div className="custom-scrollbar max-h-[280px] flex-1 overflow-y-auto pr-2">
{['C', 'E', 'F', 'G', 'I'].map((letter) => (
<div key={letter} className="flex border-b border-gray-100 py-3 last:border-0">
<div className="w-8 text-green-600 font-medium">{letter}</div>
<div className="flex flex-wrap gap-x-6 gap-y-2 flex-1">
<div key={letter} className="flex border-b border-slate-100 py-3 last:border-0">
<div className="w-8 font-medium text-emerald-600">{letter}</div>
<div className="flex flex-1 flex-wrap gap-x-3 gap-y-2 sm:gap-x-4">
{LANGUAGES.filter((l) => l.group === letter).map((lang) => (
<button
key={lang.code}
className={`text-sm hover:text-blue-600 transition-colors ${
type="button"
className={`rounded-full px-2.5 py-1 text-sm transition-colors ${
selectedTtsLanguage === lang.code
? 'bg-green-600 text-white px-2 py-0.5 rounded'
? 'bg-emerald-600 text-white'
: lang.code === 'zh' || lang.code === 'yue'
? 'text-orange-500'
: 'text-gray-700'
? 'text-orange-500 hover:bg-orange-50'
: 'text-slate-700 hover:bg-slate-100 hover:text-sky-700'
}`}
onClick={() => setSelectedTtsLanguage(lang.code)}
>
@ -252,10 +327,10 @@ export default function UploadScreen({
</div>
<button
className={`w-full py-3 rounded-md font-medium mt-4 transition-colors ${
className={`mt-5 w-full rounded-2xl px-4 py-3.5 text-sm font-semibold transition-colors sm:text-base ${
tempFile
? 'bg-[#52c41a] hover:bg-[#46a616] text-white'
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
? 'bg-[#16a34a] text-white shadow-lg shadow-emerald-200 hover:bg-[#15803d]'
: 'cursor-not-allowed bg-slate-200 text-slate-400'
}`}
onClick={() => {
if (tempFile) setShowTrimModal(true);
@ -264,8 +339,8 @@ export default function UploadScreen({
>
{m.upload.generateTranslatedVideo}
</button>
</div>
</div>
</section>
</aside>
{showTrimModal && tempFile && (
<TrimModal

View File

@ -10,6 +10,15 @@ const messages = {
productName: 'Translate 1.0',
},
upload: {
workbenchEyebrow: 'Video Translate',
workbenchTitle: '\u4e0a\u4f20\u5e76\u51c6\u5907',
workbenchDescription: '\u4e0a\u4f20\u6e90\u89c6\u9891\uff0c\u8c03\u6574\u5b57\u5e55\u9ed8\u8ba4\u6837\u5f0f\uff0c\u5e76\u5728\u751f\u6210\u524d\u9009\u62e9\u914d\u97f3\u8bbe\u7f6e\u3002',
uploadPanelTitle: '\u4e0a\u4f20\u6e90\u89c6\u9891',
uploadPanelDescription: '\u62d6\u62fd\u6216\u70b9\u51fb\u9009\u62e9\u89c6\u9891\u6587\u4ef6\uff0c\u6211\u4eec\u4f1a\u5728\u4e0b\u4e00\u6b65\u8fdb\u5165\u88c1\u526a\u548c\u7ffb\u8bd1\u6d41\u7a0b\u3002',
modeWorkflowTitle: '\u6a21\u5f0f\u4e0e\u6d41\u7a0b',
modeWorkflowDescription: '\u9009\u62e9\u4f60\u5e0c\u671b\u7684\u5de5\u4f5c\u65b9\u5f0f\uff0c\u4fdd\u6301\u5f53\u524d\u7ffb\u8bd1\u6d41\u7a0b\u4e0d\u53d8\u3002',
languageDubbingTitle: '\u8bed\u8a00\u4e0e\u914d\u97f3',
languageDubbingDescription: '\u786e\u8ba4\u5b57\u5e55\u8bed\u8a00\u4e0e TTS \u76ee\u6807\u8bed\u8a00\uff0c\u518d\u8fdb\u5165\u751f\u6210\u6d41\u7a0b\u3002',
uploadVideoFile: '\u4e0a\u4f20\u89c6\u9891\u6587\u4ef6',
clickToUpload: '\u70b9\u51fb\u4e0a\u4f20\u6216\u5c06\u89c6\u9891\u62d6\u62fd\u5230\u6b64\u5904',
uploadVideo: '\u4e0a\u4f20\u89c6\u9891',
@ -134,6 +143,19 @@ const messages = {
productName: 'Translate 1.0',
},
upload: {
workbenchEyebrow: 'Video Translate',
workbenchTitle: 'Upload & prepare',
workbenchDescription:
'Upload a source video, tune subtitle defaults, and choose dubbing settings before generation.',
uploadPanelTitle: 'Upload source video',
uploadPanelDescription:
'Drop a video here or browse from your device. We will move into trim and translation setup next.',
modeWorkflowTitle: 'Mode & Workflow',
modeWorkflowDescription:
'Choose the workflow that best fits your editing needs while keeping the translation flow intact.',
languageDubbingTitle: 'Language & Dubbing',
languageDubbingDescription:
'Confirm the subtitle language and the dubbing target before starting generation.',
uploadVideoFile: 'Upload video file',
clickToUpload: 'Click to upload or drag files here',
uploadVideo: 'Upload Video',

View File

@ -1,5 +1,10 @@
@import "tailwindcss";
.app-surface {
background: rgba(255, 255, 255, 0.82);
backdrop-filter: blur(18px);
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
height: 6px;