feat(captions): add setting to toggle timeline hover-to-add

This commit is contained in:
Joe Hachem
2026-06-28 11:49:14 +03:00
parent 1f2ddd49d3
commit bd1aff562e
16 changed files with 42 additions and 3 deletions
@@ -2778,6 +2778,18 @@ export function SettingsPanel({
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between gap-3 rounded-lg bg-foreground/[0.03] px-2.5 py-2">
<div className="text-[10px] text-muted-foreground">
{tSettings("captions.timelineQuickAdd", "Hover to add on timeline")}
</div>
<Switch
checked={autoCaptionSettings.timelineQuickAdd}
onCheckedChange={(timelineQuickAdd) =>
updateAutoCaptionSettings({ timelineQuickAdd })
}
className="data-[state=checked]:bg-[#2563EB] scale-75"
/>
</div>
<label className="flex items-center justify-between rounded-lg bg-foreground/[0.03] px-2.5 py-2">
<span className="text-[10px] text-muted-foreground">
{tSettings("captions.textColor", "Text color")}
@@ -6848,6 +6848,7 @@ export default function VideoEditor() {
onCaptionDelete={handleCaptionDelete}
onCaptionAdded={handleCaptionAdded}
captionsEnabled={autoCaptionSettings.enabled}
captionQuickAddEnabled={autoCaptionSettings.timelineQuickAdd}
annotationRegions={annotationRegions}
onAnnotationAdded={handleAnnotationAdded}
onAnnotationSpanChange={handleAnnotationSpanChange}
@@ -759,6 +759,10 @@ export function normalizeProjectEditor(editor: Partial<ProjectEditorState>): Pro
typeof rawAutoCaptionSettings.enabled === "boolean"
? rawAutoCaptionSettings.enabled
: DEFAULT_AUTO_CAPTION_SETTINGS.enabled,
timelineQuickAdd:
typeof rawAutoCaptionSettings.timelineQuickAdd === "boolean"
? rawAutoCaptionSettings.timelineQuickAdd
: DEFAULT_AUTO_CAPTION_SETTINGS.timelineQuickAdd,
language:
typeof rawAutoCaptionSettings.language === "string" &&
rawAutoCaptionSettings.language.trim()
@@ -74,6 +74,7 @@ export interface TimelineEditorProps {
onCaptionDelete?: (id: string) => void;
onCaptionAdded?: (span: Span) => void;
captionsEnabled?: boolean;
captionQuickAddEnabled?: boolean;
selectedCaptionId?: string | null;
onSelectCaption?: (id: string | null) => void;
videoPath?: string | null;
@@ -155,6 +156,7 @@ const TimelineEditor = forwardRef<TimelineEditorHandle, TimelineEditorProps>(
onCaptionDelete,
onCaptionAdded,
captionsEnabled = false,
captionQuickAddEnabled = true,
selectedCaptionId,
onSelectCaption,
videoPath,
@@ -477,6 +479,7 @@ const TimelineEditor = forwardRef<TimelineEditorHandle, TimelineEditorProps>(
onAddCaptionAtMs={addCaptionAtMs}
canPlaceCaptionAtMs={canPlaceCaptionAtMs}
captionsEnabled={captionsEnabled}
captionQuickAddEnabled={captionQuickAddEnabled}
onSelectZoom={handleSelectZoom}
onSelectClip={handleSelectClip}
onSelectAnnotation={handleSelectAnnotation}
@@ -64,6 +64,7 @@ interface TimelineCanvasProps {
onAddCaptionAtMs?: (startMs: number) => void;
canPlaceCaptionAtMs?: (startMs: number) => boolean;
captionsEnabled?: boolean;
captionQuickAddEnabled?: boolean;
selectedZoomId: string | null;
selectedClipId?: string | null;
selectedAnnotationId?: string | null;
@@ -160,12 +161,13 @@ function useTimelineLaneHover({
const onClick = useCallback(
(event: MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
if (!onAddAtMs || hoverMs === null) return;
// Respect the lane's enabled flag so a hidden ghost can't still add on click.
if (!enabled || !onAddAtMs || hoverMs === null) return;
const startMs = Math.max(0, Math.min(hoverMs, videoDurationMs));
if (canPlaceAtMs && !canPlaceAtMs(startMs)) return;
onAddAtMs(startMs);
},
[canPlaceAtMs, onAddAtMs, videoDurationMs, hoverMs],
[enabled, canPlaceAtMs, onAddAtMs, videoDurationMs, hoverMs],
);
const reset = useCallback(() => {
@@ -215,6 +217,7 @@ interface TimelineHoverParams {
onAddCaptionAtMs?: (startMs: number) => void;
canPlaceCaptionAtMs?: (startMs: number) => boolean;
captionsEnabled?: boolean;
captionQuickAddEnabled?: boolean;
isDragging: boolean;
valueToPixels: (value: number) => number;
}
@@ -230,6 +233,7 @@ function useTimelineHover({
onAddCaptionAtMs,
canPlaceCaptionAtMs,
captionsEnabled,
captionQuickAddEnabled = true,
isDragging,
valueToPixels,
}: TimelineHoverParams) {
@@ -289,7 +293,7 @@ function useTimelineHover({
videoDurationMs,
valueToPixels,
ghostDurationMs: Math.min(DEFAULT_CAPTION_DURATION_MS, videoDurationMs),
enabled: Boolean(captionsEnabled),
enabled: Boolean(captionsEnabled) && captionQuickAddEnabled,
isDragging,
onAddAtMs: onAddCaptionAtMs,
canPlaceAtMs: canPlaceCaptionAtMs,
@@ -731,6 +735,7 @@ export default function TimelineCanvas({
onAddCaptionAtMs,
canPlaceCaptionAtMs,
captionsEnabled,
captionQuickAddEnabled,
onSelectZoom,
onSelectClip,
onSelectAnnotation,
@@ -954,6 +959,7 @@ export default function TimelineCanvas({
onAddCaptionAtMs,
canPlaceCaptionAtMs,
captionsEnabled,
captionQuickAddEnabled,
isDragging,
valueToPixels,
});
+3
View File
@@ -553,6 +553,8 @@ export type AutoCaptionAnimation = "none" | "fade" | "rise" | "pop";
export interface AutoCaptionSettings {
enabled: boolean;
/** Show the hover ghost on the timeline caption track for click-to-add. */
timelineQuickAdd: boolean;
language: string;
fontFamily: string;
fontSize: number;
@@ -568,6 +570,7 @@ export interface AutoCaptionSettings {
export const DEFAULT_AUTO_CAPTION_SETTINGS: AutoCaptionSettings = {
enabled: false,
timelineQuickAdd: true,
language: "auto",
fontFamily: getDefaultCaptionFontFamily(),
fontSize: 30,
+1
View File
@@ -168,6 +168,7 @@
"captions": {
"selectOnTimeline": "Select a caption on the timeline to edit it.",
"enabled": "Show",
"timelineQuickAdd": "Hover to add on timeline",
"language": "Language",
"downloading": "Downloading...",
"deleteModel": "Delete Model",
+1
View File
@@ -120,6 +120,7 @@
"captions": {
"selectOnTimeline": "Selecciona un subtítulo en la línea de tiempo para editarlo.",
"enabled": "Mostrar",
"timelineQuickAdd": "Pasar el cursor para añadir en la línea de tiempo",
"language": "Idioma",
"downloading": "Descargando...",
"deleteModel": "Eliminar modelo",
+1
View File
@@ -120,6 +120,7 @@
"captions": {
"selectOnTimeline": "Sélectionnez un sous-titre sur la timeline pour le modifier.",
"enabled": "Afficher",
"timelineQuickAdd": "Survoler pour ajouter sur la timeline",
"language": "Langue",
"downloading": "Téléchargement...",
"deleteModel": "Supprimer le modèle",
+1
View File
@@ -142,6 +142,7 @@
"captions": {
"selectOnTimeline": "Seleziona un sottotitolo sulla timeline per modificarlo.",
"enabled": "Mostra",
"timelineQuickAdd": "Passa il mouse per aggiungere sulla timeline",
"language": "Lingua",
"downloading": "Download in corso...",
"deleteModel": "Elimina modello",
+1
View File
@@ -120,6 +120,7 @@
"captions": {
"selectOnTimeline": "타임라인에서 자막을 선택하여 편집하세요.",
"enabled": "표시",
"timelineQuickAdd": "타임라인에 마우스를 올려 추가",
"language": "언어",
"downloading": "다운로드 중...",
"deleteModel": "모델 삭제",
+1
View File
@@ -120,6 +120,7 @@
"captions": {
"selectOnTimeline": "Selecteer een ondertitel op de tijdlijn om deze te bewerken.",
"enabled": "Tonen",
"timelineQuickAdd": "Aanwijzen om toe te voegen op tijdlijn",
"language": "Taal",
"downloading": "Downloaden...",
"deleteModel": "Model verwijderen",
+1
View File
@@ -120,6 +120,7 @@
"captions": {
"selectOnTimeline": "Selecione uma legenda na linha do tempo para editá-la.",
"enabled": "Mostrar",
"timelineQuickAdd": "Passe o cursor para adicionar na linha do tempo",
"language": "Idioma",
"downloading": "Baixando...",
"deleteModel": "Excluir modelo",
+1
View File
@@ -142,6 +142,7 @@
"captions": {
"selectOnTimeline": "Выберите субтитры на таймлайне, чтобы отредактировать их.",
"enabled": "Показать",
"timelineQuickAdd": "Наведите курсор, чтобы добавить на таймлайн",
"language": "Язык",
"downloading": "Загрузка...",
"deleteModel": "Удалить модель",
+1
View File
@@ -137,6 +137,7 @@
"captions": {
"selectOnTimeline": "在时间轴上选择字幕进行编辑。",
"enabled": "显示",
"timelineQuickAdd": "悬停以在时间轴上添加",
"language": "语言",
"downloading": "下载中...",
"deleteModel": "删除模型",
+1
View File
@@ -120,6 +120,7 @@
"captions": {
"selectOnTimeline": "在時間軸上選擇字幕進行編輯。",
"enabled": "顯示",
"timelineQuickAdd": "懸停以在時間軸上新增",
"language": "語言",
"downloading": "下載中...",
"deleteModel": "刪除模型",