diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index fdc19ccbc..2aa748b49 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -752,6 +752,7 @@ interface Window { projectData: unknown, projectName: string, thumbnailDataUrl?: string | null, + mode?: "rename" | "copy", ) => Promise<{ success: boolean; path?: string; diff --git a/electron/ipc/register/project.ts b/electron/ipc/register/project.ts index a65fd9070..61dbdbaa8 100644 --- a/electron/ipc/register/project.ts +++ b/electron/ipc/register/project.ts @@ -80,6 +80,12 @@ function normalizeProjectSaveName(projectName?: string | null) { return sanitizedName || null; } +type NamedProjectSaveMode = "rename" | "copy"; + +function normalizeNamedProjectSaveMode(value: unknown): NamedProjectSaveMode { + return value === "copy" ? "copy" : "rename"; +} + /** * Extracts the persisted source video path from a saved project payload. */ @@ -292,8 +298,10 @@ export function registerProjectHandlers() { try { const projectsDir = await getProjectsDir() const preparedProject = ensureProjectDataHasProjectId(projectData) - const trustedExistingProjectPath = isTrustedProjectPath(existingProjectPath) - ? existingProjectPath + const trustedExistingProjectPath = existingProjectPath && + path.extname(existingProjectPath).toLowerCase() === `.${PROJECT_FILE_EXTENSION}` && + (isTrustedProjectPath(existingProjectPath) || isPathInsideDirectory(existingProjectPath, projectsDir)) + ? path.resolve(existingProjectPath) : null if (trustedExistingProjectPath) { @@ -309,6 +317,13 @@ export function registerProjectHandlers() { } } + if (existingProjectPath) { + return { + success: false, + message: 'Project path is no longer trusted. Use Save As to choose a project file.', + } + } + const safeName = normalizeProjectSaveName(suggestedName) || `project-${Date.now()}` const defaultName = `${safeName}.${PROJECT_FILE_EXTENSION}` @@ -351,7 +366,7 @@ export function registerProjectHandlers() { } }) - ipcMain.handle('save-project-file-named', async (_, projectData: unknown, projectName: string, thumbnailDataUrl?: string | null) => { + ipcMain.handle('save-project-file-named', async (_, projectData: unknown, projectName: string, thumbnailDataUrl?: string | null, mode?: unknown) => { try { const normalizedProjectName = normalizeProjectSaveName(projectName) if (!normalizedProjectName) { @@ -362,7 +377,7 @@ export function registerProjectHandlers() { } const projectsDir = await getProjectsDir() - const preparedProject = ensureProjectDataHasProjectId(projectData) + const namedSaveMode = normalizeNamedProjectSaveMode(mode) const activeProjectPath = isTrustedProjectPath(currentProjectPath) ? currentProjectPath : null @@ -370,6 +385,22 @@ export function registerProjectHandlers() { projectsDir, `${normalizedProjectName}.${PROJECT_FILE_EXTENSION}`, ) + const [activeResolvedPath, targetResolvedPath] = await Promise.all([ + activeProjectPath ? resolveComparablePath(activeProjectPath) : Promise.resolve(null), + resolveComparablePath(targetProjectPath), + ]) + const isSavingToDifferentPath = + !activeResolvedPath || activeResolvedPath !== targetResolvedPath + const preparedProject = + namedSaveMode === "copy" && isSavingToDifferentPath + ? (() => { + const projectId = randomUUID() + return { + projectId, + projectData: withProjectId(projectData, projectId), + } + })() + : ensureProjectDataHasProjectId(projectData) const overwriteCheck = await ensureNamedProjectSaveDoesNotOverwriteDifferentProject( targetProjectPath, @@ -384,13 +415,7 @@ export function registerProjectHandlers() { await saveProjectThumbnail(targetProjectPath, thumbnailDataUrl) await rememberRecentProject(targetProjectPath) - if (activeProjectPath) { - const [activeResolvedPath, targetResolvedPath] = await Promise.all([ - resolveComparablePath(activeProjectPath), - resolveComparablePath(targetProjectPath), - ]) - - if (activeResolvedPath !== targetResolvedPath) { + if (namedSaveMode === "rename" && activeProjectPath && isSavingToDifferentPath) { await fs.unlink(activeProjectPath).catch((unlinkError: NodeJS.ErrnoException) => { if (unlinkError.code !== 'ENOENT') { throw unlinkError @@ -407,7 +432,6 @@ export function registerProjectHandlers() { } } await saveRecentProjectPaths(filteredRecentProjectPaths) - } } setCurrentProjectPath(targetProjectPath) diff --git a/electron/preload.ts b/electron/preload.ts index c339ad3ce..9a15edb99 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -783,12 +783,14 @@ contextBridge.exposeInMainWorld("electronAPI", { projectData: unknown, projectName: string, thumbnailDataUrl?: string | null, + mode?: "rename" | "copy", ) => { return ipcRenderer.invoke( "save-project-file-named", projectData, projectName, thumbnailDataUrl, + mode, ); }, loadProjectFile: () => { diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index fce5f2b91..fbf390b4b 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -73,6 +73,7 @@ import type { ZoomTransitionEasing, } from "./types"; import { + ADVANCED_VERTICAL_PADDING_MAX, DEFAULT_AUTO_CAPTION_SETTINGS, DEFAULT_CROP_REGION, DEFAULT_CURSOR_CLICK_BOUNCE, @@ -107,6 +108,7 @@ import { } from "./videoPlayback/uploadedCursorAssets"; import { WebcamCropControl } from "./WebcamCropControl"; import { + getCropMatchedWebcamHeightPercent, getWebcamPositionForPreset, normalizeWebcamCropRegion, resolveWebcamCorner, @@ -1662,6 +1664,8 @@ export function SettingsPanel({ const webcamPositionPreset = webcam?.positionPreset ?? DEFAULT_WEBCAM_POSITION_PRESET; const webcamPositionX = webcam?.positionX ?? DEFAULT_WEBCAM_POSITION_X; const webcamPositionY = webcam?.positionY ?? DEFAULT_WEBCAM_POSITION_Y; + const webcamWidth = webcam?.width ?? webcam?.size ?? DEFAULT_WEBCAM_SIZE; + const webcamHeight = webcam?.height ?? webcam?.size ?? DEFAULT_WEBCAM_SIZE; const webcamCrop = normalizeWebcamCropRegion(webcam?.cropRegion); const getWallpaperTileState = (candidateValue: string, previewPath?: string) => { @@ -2413,7 +2417,7 @@ export function SettingsPanel({ value={padding.top} defaultValue={DEFAULT_PADDING.top} min={0} - max={100} + max={ADVANCED_VERTICAL_PADDING_MAX} step={1} onChange={(v) => handlePaddingSideChange("top", v)} formatValue={(v) => `${v}%`} @@ -2424,7 +2428,7 @@ export function SettingsPanel({ value={padding.bottom} defaultValue={DEFAULT_PADDING.bottom} min={0} - max={100} + max={ADVANCED_VERTICAL_PADDING_MAX} step={1} onChange={(v) => handlePaddingSideChange("bottom", v)} formatValue={(v) => `${v}%`} @@ -3871,13 +3875,24 @@ export function SettingsPanel({ /> updateWebcam({ size: v })} + onChange={(v) => updateWebcam({ width: v, size: v })} + formatValue={(v) => `${Math.round(v)}%`} + parseInput={(text) => parseFloat(text.replace(/%$/, ""))} + /> + updateWebcam({ height: v })} formatValue={(v) => `${Math.round(v)}%`} parseInput={(text) => parseFloat(text.replace(/%$/, ""))} /> @@ -3903,7 +3918,20 @@ export function SettingsPanel({ previewCurrentTime={webcamPreviewCurrentTime} previewPlaying={webcamPreviewPlaying} previewTimeOffsetMs={webcam?.timeOffsetMs} - onCropChange={(cropRegion) => updateWebcam({ cropRegion })} + onCropChange={(cropRegion, previewFrame) => + updateWebcam({ + cropRegion, + height: previewFrame + ? getCropMatchedWebcamHeightPercent( + webcamWidth, + webcamHeight, + previewFrame.width, + previewFrame.height, + cropRegion, + ) + : webcamHeight, + }) + } />
diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index e26fe4231..827dcb30c 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -118,8 +118,8 @@ import type { SourceAudioTrackSettings } from "@/components/video-editor/audio/a import { extensionHost } from "@/lib/extensions"; import { useVideoEditorAudio } from "./audio/useVideoEditorAudio"; import { resolveAutoCaptionSourcePath } from "./autoCaptionSource"; -import { CropControl } from "./CropControl"; import { type CaptionEditTarget, updateCaptionCuesForEditedTarget } from "./captionEditing"; +import { CropControl } from "./CropControl"; import { ExportSettingsMenu } from "./ExportSettingsMenu"; import ExtensionManager from "./ExtensionManager"; import { @@ -299,10 +299,18 @@ type SaveProjectOptions = { captureThumbnail?: boolean; }; +type NamedProjectSaveMode = "rename" | "copy"; + type PendingProjectSaveDialog = { resolve: (saved: boolean) => void; }; +type PendingUnsavedChangesDialogDecision = "cancel" | "discard" | "save"; + +type PendingUnsavedChangesDialog = { + resolve: (decision: PendingUnsavedChangesDialogDecision) => void; +}; + async function writeSmokeExportReport( outputPath: string | null, report: Record, @@ -390,6 +398,10 @@ export default function VideoEditor() { const [projectSaveDialogOpen, setProjectSaveDialogOpen] = useState(false); const [projectSaveDialogDraft, setProjectSaveDialogDraft] = useState(""); const [isSavingProjectDialog, setIsSavingProjectDialog] = useState(false); + const [unsavedChangesDialogOpen, setUnsavedChangesDialogOpen] = useState(false); + const [unsavedChangesDialogActionLabel, setUnsavedChangesDialogActionLabel] = useState( + "continue", + ); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [isPlaying, setIsPlaying] = useState(false); @@ -678,6 +690,7 @@ export default function VideoEditor() { const smokeExportStartedRef = useRef(false); const projectAutosaveTimeoutRef = useRef(null); const pendingProjectSaveDialogRef = useRef(null); + const pendingUnsavedChangesDialogRef = useRef(null); const projectSaveQueueRef = useRef>(Promise.resolve()); const smokeExportReadyStateRef = useRef>({}); const [historyVersion, setHistoryVersion] = useState(0); @@ -2171,6 +2184,26 @@ export default function VideoEditor() { }); }, []); + const resolveUnsavedChangesDialog = useCallback( + (decision: PendingUnsavedChangesDialogDecision) => { + const pendingDialog = pendingUnsavedChangesDialogRef.current; + pendingUnsavedChangesDialogRef.current = null; + setUnsavedChangesDialogOpen(false); + pendingDialog?.resolve(decision); + }, + [], + ); + + const openUnsavedChangesDialog = useCallback((actionLabel: string) => { + pendingUnsavedChangesDialogRef.current?.resolve("cancel"); + setUnsavedChangesDialogActionLabel(actionLabel); + setUnsavedChangesDialogOpen(true); + + return new Promise((resolve) => { + pendingUnsavedChangesDialogRef.current = { resolve }; + }); + }, []); + const syncRecordingSessionWebcam = useCallback( async (webcamPath: string | null, timeOffsetMs?: number) => { if (!currentSourcePath || !window.electronAPI.setCurrentRecordingSession) { @@ -2384,6 +2417,14 @@ export default function VideoEditor() { smokeExportConfig.webcamSize === undefined ? prev.size : smokeExportConfig.webcamSize, + width: + smokeExportConfig.webcamSize === undefined + ? (prev.width ?? prev.size) + : smokeExportConfig.webcamSize, + height: + smokeExportConfig.webcamSize === undefined + ? (prev.height ?? prev.size) + : smokeExportConfig.webcamSize, })); setError(null); return; @@ -3006,7 +3047,7 @@ export default function VideoEditor() { * Saves the current project directly into the projects library under a chosen name. */ const saveProjectWithName = useCallback( - async (projectName: string) => { + async (projectName: string, mode: NamedProjectSaveMode = "rename") => { const trimmedProjectName = projectName.trim(); if (!trimmedProjectName) { toast.error("Project name is required"); @@ -3032,6 +3073,7 @@ export default function VideoEditor() { projectData, trimmedProjectName, thumbnailDataUrl, + mode, ); if (result.canceled) { @@ -3088,7 +3130,7 @@ export default function VideoEditor() { setIsSavingProjectDialog(true); let saved = false; try { - saved = await saveProjectWithName(trimmedProjectName); + saved = await saveProjectWithName(trimmedProjectName, "copy"); } catch (error) { toast.error(getErrorMessage(error)); } finally { @@ -3129,7 +3171,7 @@ export default function VideoEditor() { setIsSavingProjectName(true); let saved = false; try { - saved = await saveProjectWithName(trimmedProjectName); + saved = await saveProjectWithName(trimmedProjectName, "rename"); } catch (error) { toast.error(getErrorMessage(error)); } finally { @@ -3147,8 +3189,32 @@ export default function VideoEditor() { [closeProjectNameEditor, projectNameDraft, saveProjectWithName], ); + const confirmReplaceSourceWithUnsavedChanges = useCallback( + async (actionLabel: string) => { + if (!hasUnsavedChanges) { + return true; + } + + const decision = await openUnsavedChangesDialog(actionLabel); + if (decision === "discard") { + return true; + } + + if (decision === "save") { + return saveProject(false); + } + + return false; + }, + [hasUnsavedChanges, openUnsavedChangesDialog, saveProject], + ); + const handleOpenProjectFromLibrary = useCallback( async (projectPath: string) => { + if (!(await confirmReplaceSourceWithUnsavedChanges("open another project"))) { + return; + } + const result = await window.electronAPI.openProjectFileAtPath(projectPath); if (result.canceled) { @@ -3170,10 +3236,14 @@ export default function VideoEditor() { await refreshProjectLibrary(); toast.success(`Project loaded from ${result.path}`); }, - [applyLoadedProject, refreshProjectLibrary], + [applyLoadedProject, confirmReplaceSourceWithUnsavedChanges, refreshProjectLibrary], ); const handleImportMediaOrProject = useCallback(async () => { + if (!(await confirmReplaceSourceWithUnsavedChanges("import a file"))) { + return; + } + const result = await window.electronAPI.openVideoFilePicker({ includeProjects: true }); if (result.canceled) { @@ -3237,6 +3307,7 @@ export default function VideoEditor() { applyLoadedProject, applySessionPresentation, autoApplyFreshRecordingAutoZooms, + confirmReplaceSourceWithUnsavedChanges, refreshProjectLibrary, resetSourceScopedEditorState, ]); @@ -5446,6 +5517,48 @@ export default function VideoEditor() { ); + const unsavedChangesDialog = ( + { + if (open) { + setUnsavedChangesDialogOpen(true); + return; + } + + resolveUnsavedChangesDialog("cancel"); + }} + > + + + Unsaved changes + + {`Save your current project before you ${unsavedChangesDialogActionLabel}?`} + + + + + + + + + + ); + const projectBrowser = ( Loading video...
{projectBrowser} {projectSaveDialog} + {unsavedChangesDialog} {nativeCaptureUnavailableDialog} @@ -5516,6 +5630,7 @@ export default function VideoEditor() { {projectBrowser} {projectSaveDialog} + {unsavedChangesDialog} {nativeCaptureUnavailableDialog} @@ -6645,6 +6760,7 @@ export default function VideoEditor() { {projectBrowser} {projectSaveDialog} + {unsavedChangesDialog} {nativeCaptureUnavailableDialog} diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 9e3a22287..4c739f1c1 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -167,22 +167,28 @@ import { SNAP_TO_EDGES_RATIO_AUTO, } from "./videoPlayback/cursorFollowCamera"; import { clampFocusToStage as clampFocusToStageUtil } from "./videoPlayback/focusUtils"; -import { layoutVideoContent as layoutVideoContentUtil } from "./videoPlayback/layoutUtils"; +import { + layoutVideoContent as layoutVideoContentUtil, + scalePreviewBorderRadius, +} from "./videoPlayback/layoutUtils"; import { updateOverlayIndicator } from "./videoPlayback/overlayUtils"; import { createVideoEventHandlers } from "./videoPlayback/videoEventHandlers"; -import { getWebcamMediaTargetTimeSeconds, shouldSeekWebcamMedia } from "./videoPlayback/webcamSync"; +import { + getWebcamMediaTargetTimeSeconds, + shouldSeekWebcamMedia, +} from "./videoPlayback/webcamSync"; import { findDominantRegion } from "./videoPlayback/zoomRegionUtils"; import { applyZoomTransform, - computeFocusFromTransform, computeZoomTransform, createMotionBlurState, type MotionBlurState, } from "./videoPlayback/zoomTransform"; import { + getCropMatchedWebcamHeightPercent, getWebcamCropSourceRect, + getWebcamOverlayDimensionsPx, getWebcamOverlayPosition, - getWebcamOverlaySizePx, } from "./webcamOverlay"; type PlaybackAnimationState = { @@ -925,7 +931,8 @@ const VideoPlayback = forwardRef( const motionBlurStateRef = useRef(createMotionBlurState()); const webcamEnabled = webcam?.enabled ?? false; const webcamMargin = webcam?.margin ?? 24; - const webcamSize = webcam?.size ?? DEFAULT_WEBCAM_SIZE; + const webcamWidth = webcam?.width ?? webcam?.size ?? DEFAULT_WEBCAM_SIZE; + const rawWebcamHeight = webcam?.height ?? webcam?.size ?? DEFAULT_WEBCAM_SIZE; const webcamReactToZoom = webcam?.reactToZoom ?? DEFAULT_WEBCAM_REACT_TO_ZOOM; const webcamPositionPreset = webcam?.positionPreset ?? webcam?.corner ?? "bottom-right"; const webcamPositionX = webcam?.positionX ?? 1; @@ -936,6 +943,13 @@ const VideoPlayback = forwardRef( const webcamTimeOffsetMs = webcam?.timeOffsetMs; const webcamCropRegion = webcam?.cropRegion; const webcamMirror = webcam?.mirror ?? false; + const webcamHeight = getCropMatchedWebcamHeightPercent( + webcamWidth, + rawWebcamHeight, + webcamVideoDimensions?.width, + webcamVideoDimensions?.height, + webcamCropRegion, + ); const webcamCropPreviewContentStyle = useMemo(() => { if (!webcamVideoDimensions) { return { opacity: 0 }; @@ -946,21 +960,22 @@ const VideoPlayback = forwardRef( webcamVideoDimensions.width, webcamVideoDimensions.height, ); - const coverScale = Math.max(1 / sw, 1 / sh); + const targetAspect = Math.max(0.01, webcamWidth) / Math.max(0.01, webcamHeight); + const coverScale = Math.max(targetAspect / sw, 1 / sh); const drawWidth = webcamVideoDimensions.width * coverScale; const drawHeight = webcamVideoDimensions.height * coverScale; - const drawX = (1 - sw * coverScale) / 2 - sx * coverScale; + const drawX = (targetAspect - sw * coverScale) / 2 - sx * coverScale; const drawY = (1 - sh * coverScale) / 2 - sy * coverScale; return { - left: `${drawX * 100}%`, + left: `${(drawX / targetAspect) * 100}%`, top: `${drawY * 100}%`, - width: `${drawWidth * 100}%`, + width: `${(drawWidth / targetAspect) * 100}%`, height: `${drawHeight * 100}%`, maxWidth: "none", willChange: "left, top, width, height", }; - }, [webcamCropRegion, webcamVideoDimensions]); + }, [webcamCropRegion, webcamHeight, webcamVideoDimensions, webcamWidth]); const applyWebcamBubbleLayout = useCallback( (zoomScale: number) => { @@ -974,10 +989,11 @@ const VideoPlayback = forwardRef( return; } - const scaledSize = getWebcamOverlaySizePx({ + const scaledDimensions = getWebcamOverlayDimensionsPx({ containerWidth: overlay.clientWidth, containerHeight: overlay.clientHeight, - sizePercent: webcamSize, + widthPercent: webcamWidth, + heightPercent: webcamHeight, margin: webcamMargin, zoomScale, reactToZoom: webcamReactToZoom, @@ -985,7 +1001,8 @@ const VideoPlayback = forwardRef( const { x, y } = getWebcamOverlayPosition({ containerWidth: overlay.clientWidth, containerHeight: overlay.clientHeight, - size: scaledSize, + width: scaledDimensions.width, + height: scaledDimensions.height, margin: webcamMargin, positionPreset: webcamPositionPreset, positionX: webcamPositionX, @@ -996,18 +1013,19 @@ const VideoPlayback = forwardRef( bubble.style.display = "block"; bubble.style.left = `${x}px`; bubble.style.top = `${y}px`; - bubble.style.width = `${scaledSize}px`; - bubble.style.height = `${scaledSize}px`; - bubble.style.aspectRatio = "1 / 1"; + bubble.style.width = `${scaledDimensions.width}px`; + bubble.style.height = `${scaledDimensions.height}px`; + bubble.style.aspectRatio = `${scaledDimensions.width} / ${scaledDimensions.height}`; const squirclePath = getSquircleSvgPath({ x: 0, y: 0, - width: scaledSize, - height: scaledSize, + width: scaledDimensions.width, + height: scaledDimensions.height, radius: webcamCornerRadius, }); - bubble.style.filter = `drop-shadow(0 ${Math.round(scaledSize * 0.06)}px ${Math.round( - scaledSize * 0.22, + const shadowSize = Math.min(scaledDimensions.width, scaledDimensions.height); + bubble.style.filter = `drop-shadow(0 ${Math.round(shadowSize * 0.06)}px ${Math.round( + shadowSize * 0.22, )}px rgba(0, 0, 0, ${webcamShadow}))`; bubble.style.borderRadius = "0px"; bubble.style.boxShadow = "none"; @@ -1028,8 +1046,9 @@ const VideoPlayback = forwardRef( webcamPositionY, webcamReactToZoom, webcamShadow, - webcamSize, + webcamHeight, webcamVideoPath, + webcamWidth, ], ); @@ -2157,6 +2176,7 @@ const VideoPlayback = forwardRef( if (cursorOverlayEnabled) { const cursorOverlay = new PixiCursorOverlay({ dotRadius: DEFAULT_CURSOR_CONFIG.dotRadius * cursorSizeRef.current, + minViewportScale: 0, style: cursorStyleRef.current, smoothingFactor: cursorSmoothingRef.current, springTuning: { @@ -2389,7 +2409,7 @@ const VideoPlayback = forwardRef( return; } - const { region, strength, blendedScale, transition } = findDominantRegion( + const { region, strength, blendedScale } = findDominantRegion( zoomRegionsRef.current, currentTimeRef.current, { @@ -2434,47 +2454,6 @@ const VideoPlayback = forwardRef( targetScaleFactor = zoomScale; targetFocus = regionFocus; targetProgress = strength; - - if (transition) { - const startTransform = computeZoomTransform({ - stageSize: stageSizeRef.current, - baseMask: baseMaskRef.current, - zoomScale: transition.startScale, - zoomProgress: 1, - focusX: transition.startFocus.cx, - focusY: transition.startFocus.cy, - }); - const endTransform = computeZoomTransform({ - stageSize: stageSizeRef.current, - baseMask: baseMaskRef.current, - zoomScale: transition.endScale, - zoomProgress: 1, - focusX: transition.endFocus.cx, - focusY: transition.endFocus.cy, - }); - - const interpolatedTransform = { - scale: - startTransform.scale + - (endTransform.scale - startTransform.scale) * transition.progress, - x: - startTransform.x + - (endTransform.x - startTransform.x) * transition.progress, - y: - startTransform.y + - (endTransform.y - startTransform.y) * transition.progress, - }; - - targetScaleFactor = interpolatedTransform.scale; - targetFocus = computeFocusFromTransform({ - stageSize: stageSizeRef.current, - baseMask: baseMaskRef.current, - zoomScale: interpolatedTransform.scale, - x: interpolatedTransform.x, - y: interpolatedTransform.y, - }); - targetProgress = 1; - } } const state = animationStateRef.current; @@ -3138,14 +3117,14 @@ const VideoPlayback = forwardRef( ) : null} - {activeCaptionLayout && autoCaptionSettings ? ( -
+ {activeCaptionLayout && autoCaptionSettings ? ( +
( >
{ - event.stopPropagation(); - if (!isCaptionEditing) { - beginCaptionEdit(); - } - }} - onPointerDown={(event) => { - event.stopPropagation(); - }} - onKeyDown={(event) => { - if (!onEditAutoCaption || isCaptionEditing) { - return; - } + role={ + onEditAutoCaption && !isCaptionEditing ? "button" : undefined + } + tabIndex={onEditAutoCaption && !isCaptionEditing ? 0 : undefined} + aria-label={ + onEditAutoCaption && !isCaptionEditing ? "Edit current caption" : undefined + } + onClick={(event) => { + event.stopPropagation(); + if (!isCaptionEditing) { + beginCaptionEdit(); + } + }} + onPointerDown={(event) => { + event.stopPropagation(); + }} + onKeyDown={(event) => { + if (!onEditAutoCaption || isCaptionEditing) { + return; + } - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - beginCaptionEdit(); - } - }} + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + beginCaptionEdit(); + } + }} style={{ backgroundColor: `rgba(0, 0, 0, ${autoCaptionSettings.backgroundOpacity})`, fontFamily: getDefaultCaptionFontFamily(), @@ -3226,138 +3199,122 @@ const VideoPlayback = forwardRef( ), )}px`, boxSizing: "border-box", - cursor: - onEditAutoCaption && !isCaptionEditing - ? "text" - : undefined, - pointerEvents: onEditAutoCaption ? "auto" : undefined, + cursor: + onEditAutoCaption && !isCaptionEditing ? "text" : undefined, + pointerEvents: onEditAutoCaption ? "auto" : undefined, }} > - {captionEditSession ? ( -