'use client'; import { memo, useCallback, useMemo, useRef, useState } from 'react'; import { Captions, Loader2, Trash2, Upload } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { cn } from '@/lib/utils'; import type { SubtitleTrackOption } from '@/components/video-page/types'; const COMMON_LANGUAGES = [ 'tr', 'en', 'de', 'fr', 'es', 'it', 'pt', 'nl', 'pl', 'ru', 'ar', 'ja', 'ko', 'zh', 'hi', ] as const; const OTHER_LANGUAGE = '__other__'; /** A file named `cut-v3.tr.srt` already says which language it is. */ const LANGUAGE_FROM_FILENAME = /\.([a-z]{2,3}(?:-[a-z0-9]{2,8})?)\.(?:srt|vtt)$/i; function describeLanguage(tag: string): string { try { const displayNames = new Intl.DisplayNames(undefined, { type: 'language' }); return displayNames.of(tag) || tag.toUpperCase(); } catch { return tag.toUpperCase(); } } function guessLanguageFromFileName(fileName: string): string | null { const match = LANGUAGE_FROM_FILENAME.exec(fileName); return match ? match[1].toLowerCase() : null; } interface SubtitleControlsProps { /** * What the menu lists. For a Bunny or R2 version these are the tracks uploaded to this * cut; for a YouTube version they are the captions the video already carries, which is * why the shape is narrower than a stored subtitle. */ subtitles: SubtitleTrackOption[]; activeSubtitleLanguage: string | null; onSelectSubtitleLanguage: (language: string | null) => void; canManageSubtitles: boolean; onUploadSubtitle: (file: File, language: string, label: string) => Promise; onDeleteSubtitle: (subtitleId: string) => Promise; isUploadingSubtitle: boolean; } export const SubtitleControls = memo(function SubtitleControls({ subtitles, activeSubtitleLanguage, onSelectSubtitleLanguage, canManageSubtitles, onUploadSubtitle, onDeleteSubtitle, isUploadingSubtitle, }: SubtitleControlsProps) { const fileInputRef = useRef(null); const [pendingFile, setPendingFile] = useState(null); const [languageChoice, setLanguageChoice] = useState('tr'); const [customLanguage, setCustomLanguage] = useState(''); const [label, setLabel] = useState(''); const activeSubtitle = useMemo( () => subtitles.find((subtitle) => subtitle.language === activeSubtitleLanguage) ?? null, [activeSubtitleLanguage, subtitles] ); const resolvedLanguage = ( languageChoice === OTHER_LANGUAGE ? customLanguage : languageChoice ).trim(); const handleFileChosen = useCallback((event: React.ChangeEvent) => { const file = event.target.files?.[0] ?? null; // Clearing the input lets the same file be picked again after a failed upload. event.target.value = ''; if (!file) return; const guessed = guessLanguageFromFileName(file.name); const known = guessed && (COMMON_LANGUAGES as readonly string[]).includes(guessed); setLanguageChoice(known ? (guessed as string) : guessed ? OTHER_LANGUAGE : 'tr'); setCustomLanguage(known ? '' : (guessed ?? '')); setLabel(''); setPendingFile(file); }, []); const handleUpload = useCallback(async () => { if (!pendingFile || !resolvedLanguage) return; const finalLabel = label.trim() || describeLanguage(resolvedLanguage); const error = await onUploadSubtitle(pendingFile, resolvedLanguage, finalLabel); if (error) { toast.error(error); return; } toast.success('Subtitle added'); setPendingFile(null); }, [label, onUploadSubtitle, pendingFile, resolvedLanguage]); const handleDelete = useCallback( async (subtitle: SubtitleTrackOption) => { const error = await onDeleteSubtitle(subtitle.id); if (error) { toast.error(error); return; } toast.success(`${subtitle.label} removed`); }, [onDeleteSubtitle] ); if (subtitles.length === 0 && !canManageSubtitles) return null; const replacesExisting = subtitles.some( (subtitle) => subtitle.language === resolvedLanguage.toLowerCase() ); return ( <> onSelectSubtitleLanguage(null)} className={cn(!activeSubtitleLanguage && 'font-bold text-primary')} > Off {subtitles.map((subtitle) => ( onSelectSubtitleLanguage(subtitle.language)} className={cn( 'flex items-center justify-between gap-2', subtitle.language === activeSubtitleLanguage && 'font-bold text-primary' )} > {subtitle.label} {subtitle.canDelete && ( )} ))} {canManageSubtitles && ( <> {subtitles.length > 0 && } fileInputRef.current?.click()} disabled={isUploadingSubtitle} > Add subtitle )} { if (!open && !isUploadingSubtitle) setPendingFile(null); }} > Add subtitle {pendingFile?.name} is attached to this version only, because cue timings belong to one cut. SRT files are converted to WebVTT on upload.
{languageChoice === OTHER_LANGUAGE && ( setCustomLanguage(event.target.value)} placeholder="Language tag, e.g. en-US" maxLength={20} /> )}
setLabel(event.target.value)} placeholder={resolvedLanguage ? describeLanguage(resolvedLanguage) : 'Türkçe'} maxLength={60} />
{replacesExisting && (

This version already has a track in that language. Uploading replaces it.

)}
); });