mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 23:05:49 +00:00
fix(editor): restore inline project save and preview playback stability
This commit is contained in:
@@ -16,7 +16,7 @@ import { useI18n } from "@/contexts/I18nContext";
|
||||
import { ASPECT_RATIOS, getAspectRatioLabel, getAspectRatioValue } from "@/utils/aspectRatioUtils";
|
||||
import { CropControl } from "./CropControl";
|
||||
import { EditorToolbar } from "./EditorToolbar";
|
||||
import TimelineEditor, { type TimelineEditorHandle } from "./timeline/TimelineEditor";
|
||||
import type { TimelineEditorHandle } from "./timeline/TimelineEditor";
|
||||
import VideoPlayback, { VideoPlaybackRef } from "./VideoPlayback";
|
||||
import type { CursorTelemetryPoint } from "./types";
|
||||
import type { useEditorPreferences } from "./hooks/useEditorPreferences";
|
||||
@@ -41,8 +41,6 @@ interface EditorContentProps {
|
||||
isCropped: boolean;
|
||||
hasSourceAudioFallback: boolean;
|
||||
effectiveCursorTelemetry: CursorTelemetryPoint[];
|
||||
normalizedCursorTelemetry: CursorTelemetryPoint[];
|
||||
autoSuggestZoomsTrigger: number;
|
||||
videoPlaybackRef: React.RefObject<VideoPlaybackRef | null>;
|
||||
timelineRef: React.RefObject<TimelineEditorHandle | null>;
|
||||
setDuration: (v: number) => void;
|
||||
@@ -56,7 +54,6 @@ interface EditorContentProps {
|
||||
handleOpenCropEditor: () => void;
|
||||
handleCloseCropEditor: () => void;
|
||||
handleCancelCropEditor: () => void;
|
||||
handleAutoSuggestZoomsConsumed: () => void;
|
||||
}
|
||||
|
||||
export function EditorContent({
|
||||
@@ -73,8 +70,6 @@ export function EditorContent({
|
||||
isCropped,
|
||||
hasSourceAudioFallback,
|
||||
effectiveCursorTelemetry,
|
||||
normalizedCursorTelemetry,
|
||||
autoSuggestZoomsTrigger,
|
||||
videoPlaybackRef,
|
||||
timelineRef,
|
||||
setDuration,
|
||||
@@ -88,13 +83,11 @@ export function EditorContent({
|
||||
handleOpenCropEditor,
|
||||
handleCloseCropEditor,
|
||||
handleCancelCropEditor,
|
||||
handleAutoSuggestZoomsConsumed,
|
||||
}: EditorContentProps) {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-3">
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-3">
|
||||
{/* Preview */}
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<div className="relative flex flex-1 min-h-0 flex-col overflow-hidden">
|
||||
@@ -248,55 +241,6 @@ export function EditorContent({
|
||||
togglePlayPause={togglePlayPause}
|
||||
handleSeek={handleSeek}
|
||||
/>
|
||||
</div>
|
||||
{/* Timeline */}
|
||||
<div
|
||||
className="flex-shrink-0 flex flex-col"
|
||||
style={{
|
||||
height: timelineCollapsed ? undefined : "15%",
|
||||
minHeight: timelineCollapsed ? 0 : 160,
|
||||
}}
|
||||
>
|
||||
<TimelineEditor
|
||||
ref={timelineRef as React.Ref<TimelineEditorHandle>}
|
||||
hideToolbar
|
||||
videoDuration={duration}
|
||||
currentTime={currentTime}
|
||||
playheadTime={regions.timelinePlayheadTime}
|
||||
onSeek={handleSeek}
|
||||
videoPath={videoPath}
|
||||
cursorTelemetry={normalizedCursorTelemetry}
|
||||
autoSuggestZoomsTrigger={autoSuggestZoomsTrigger}
|
||||
onAutoSuggestZoomsConsumed={handleAutoSuggestZoomsConsumed}
|
||||
zoomRegions={regions.zoomRegions}
|
||||
onZoomAdded={regions.handleZoomAdded}
|
||||
onZoomSuggested={regions.handleZoomSuggested}
|
||||
onZoomSpanChange={regions.handleZoomSpanChange}
|
||||
onZoomDelete={regions.handleZoomDelete}
|
||||
selectedZoomId={regions.selectedZoomId}
|
||||
onSelectZoom={regions.handleSelectZoom}
|
||||
trimRegions={regions.trimRegions}
|
||||
clipRegions={regions.clipRegions}
|
||||
onClipSplit={regions.handleClipSplit}
|
||||
onClipSpanChange={regions.handleClipSpanChange}
|
||||
onClipDelete={regions.handleClipDelete}
|
||||
selectedClipId={regions.selectedClipId}
|
||||
onSelectClip={regions.handleSelectClip}
|
||||
audioRegions={regions.audioRegions}
|
||||
onAudioAdded={regions.handleAudioAdded}
|
||||
onAudioSpanChange={regions.handleAudioSpanChange}
|
||||
onAudioDelete={regions.handleAudioDelete}
|
||||
selectedAudioId={regions.selectedAudioId}
|
||||
onSelectAudio={regions.handleSelectAudio}
|
||||
annotationRegions={regions.annotationRegions}
|
||||
onAnnotationAdded={regions.handleAnnotationAdded}
|
||||
onAnnotationSpanChange={regions.handleAnnotationSpanChange}
|
||||
onAnnotationDelete={regions.handleAnnotationDelete}
|
||||
selectedAnnotationId={regions.selectedAnnotationId}
|
||||
onSelectAnnotation={regions.handleSelectAnnotation}
|
||||
aspectRatio={prefs.aspectRatio}
|
||||
/>
|
||||
</div>
|
||||
{/* Crop modal */}
|
||||
{showCropModal ? (
|
||||
<>
|
||||
@@ -341,6 +285,6 @@ export function EditorContent({
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,9 @@ import {
|
||||
DownloadSimple as Download,
|
||||
FolderOpen,
|
||||
ArrowClockwise as Redo2,
|
||||
FloppyDisk as Save,
|
||||
ArrowCounterClockwise as Undo2,
|
||||
} from "@phosphor-icons/react";
|
||||
import { useMemo } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -41,7 +40,6 @@ interface EditorHeaderProps {
|
||||
headerLeftControlsPaddingClass: string;
|
||||
mp4OutputDimensions: Record<string, { width: number; height: number }>;
|
||||
gifOutputDimensions: { width: number; height: number };
|
||||
openRecordingsFolder: () => Promise<void>;
|
||||
revealExportedFile: () => Promise<void>;
|
||||
projectBrowserTriggerRef: React.RefObject<HTMLButtonElement | null>;
|
||||
}
|
||||
@@ -55,13 +53,65 @@ export function EditorHeader({
|
||||
headerLeftControlsPaddingClass,
|
||||
mp4OutputDimensions,
|
||||
gifOutputDimensions,
|
||||
openRecordingsFolder,
|
||||
revealExportedFile,
|
||||
projectBrowserTriggerRef,
|
||||
}: EditorHeaderProps) {
|
||||
const { t } = useI18n();
|
||||
const [isEditingProjectName, setIsEditingProjectName] = useState(false);
|
||||
const [projectNameDraft, setProjectNameDraft] = useState(projectDisplayName);
|
||||
const [isSavingProjectName, setIsSavingProjectName] = useState(false);
|
||||
const projectNameInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditingProjectName) {
|
||||
setProjectNameDraft(projectDisplayName);
|
||||
}
|
||||
}, [isEditingProjectName, projectDisplayName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditingProjectName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const frameId = window.requestAnimationFrame(() => {
|
||||
projectNameInputRef.current?.focus();
|
||||
projectNameInputRef.current?.select();
|
||||
});
|
||||
|
||||
return () => {
|
||||
window.cancelAnimationFrame(frameId);
|
||||
};
|
||||
}, [isEditingProjectName]);
|
||||
|
||||
const closeProjectNameEditor = useCallback(() => {
|
||||
setProjectNameDraft(projectDisplayName);
|
||||
setIsEditingProjectName(false);
|
||||
}, [projectDisplayName]);
|
||||
|
||||
const handleProjectNameSubmit = useCallback(
|
||||
async (event?: React.FormEvent<HTMLFormElement>) => {
|
||||
event?.preventDefault();
|
||||
const trimmedProjectName = projectNameDraft.trim();
|
||||
if (!trimmedProjectName) {
|
||||
closeProjectNameEditor();
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSavingProjectName(true);
|
||||
const saved = await project.saveProjectWithName(trimmedProjectName);
|
||||
setIsSavingProjectName(false);
|
||||
|
||||
if (saved) {
|
||||
setIsEditingProjectName(false);
|
||||
return;
|
||||
}
|
||||
|
||||
projectNameInputRef.current?.focus();
|
||||
projectNameInputRef.current?.select();
|
||||
},
|
||||
[closeProjectNameEditor, project, projectNameDraft],
|
||||
);
|
||||
|
||||
// ── Export derived labels ─────────────────────────────────────────
|
||||
const isLightningExportInProgress =
|
||||
prefs.exportFormat === "mp4" &&
|
||||
prefs.exportPipelineModel === "modern" &&
|
||||
@@ -123,7 +173,7 @@ export function EditorHeader({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative flex h-11 flex-shrink-0 items-center justify-between bg-editor-header/88 px-5 backdrop-blur-md border-b border-foreground/10 z-50"
|
||||
className="relative z-50 flex h-11 flex-shrink-0 items-center justify-between border-b border-foreground/10 bg-editor-header/88 px-5 backdrop-blur-md"
|
||||
style={{ WebkitAppRegion: "drag" } as React.CSSProperties}
|
||||
>
|
||||
<div
|
||||
@@ -131,13 +181,14 @@ export function EditorHeader({
|
||||
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
|
||||
>
|
||||
<Button
|
||||
ref={projectBrowserTriggerRef as React.Ref<HTMLButtonElement>}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => void openRecordingsFolder()}
|
||||
onClick={project.handleOpenProjectBrowser}
|
||||
className={APP_HEADER_ICON_BUTTON_CLASS}
|
||||
title={t("common.app.manageRecordings", "Open recordings folder")}
|
||||
aria-label={t("common.app.manageRecordings", "Open recordings folder")}
|
||||
title={t("editor.project.projects", "Open projects")}
|
||||
aria-label={t("editor.project.projects", "Open projects")}
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -168,48 +219,66 @@ export function EditorHeader({
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className="pointer-events-none absolute left-1/2 flex min-w-0 -translate-x-1/2 items-baseline justify-center gap-0"
|
||||
style={{ WebkitAppRegion: "drag" } as React.CSSProperties}
|
||||
className="absolute left-1/2 flex min-w-0 -translate-x-1/2 items-center justify-center"
|
||||
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
|
||||
>
|
||||
<span className="text-sm font-semibold tracking-tight text-foreground/90">
|
||||
{projectDisplayName}
|
||||
</span>
|
||||
<span className="text-xs font-medium tracking-tight text-muted-foreground/70">
|
||||
.recordly
|
||||
</span>
|
||||
{isEditingProjectName ? (
|
||||
<form
|
||||
onSubmit={(event) => void handleProjectNameSubmit(event)}
|
||||
className="flex max-w-[min(52vw,460px)] items-baseline gap-1 rounded-[7px] border border-foreground/10 bg-editor-panel/[0.88] px-2.5 py-1 shadow-[0_10px_28px_rgba(0,0,0,0.18)]"
|
||||
>
|
||||
{project.hasUnsavedChanges ? (
|
||||
<span className="mt-[1px] size-2 shrink-0 rounded-full bg-[#2563EB]" />
|
||||
) : null}
|
||||
<input
|
||||
ref={projectNameInputRef}
|
||||
type="text"
|
||||
value={projectNameDraft}
|
||||
onChange={(event) => setProjectNameDraft(event.target.value)}
|
||||
onBlur={() => {
|
||||
if (!isSavingProjectName) {
|
||||
closeProjectNameEditor();
|
||||
}
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
closeProjectNameEditor();
|
||||
}
|
||||
}}
|
||||
disabled={isSavingProjectName}
|
||||
className="min-w-[10ch] max-w-[min(40vw,360px)] bg-transparent text-sm font-semibold tracking-tight text-foreground/95 outline-none placeholder:text-muted-foreground/60 disabled:cursor-wait"
|
||||
style={{ width: `${Math.max(projectNameDraft.length, 10)}ch` }}
|
||||
aria-label={t("editor.project.renameInput", "Project name")}
|
||||
/>
|
||||
<span className="shrink-0 text-xs font-medium tracking-tight text-muted-foreground/70">
|
||||
.recordly
|
||||
</span>
|
||||
</form>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsEditingProjectName(true)}
|
||||
className="inline-flex max-w-[min(52vw,460px)] items-baseline gap-1 rounded-[7px] px-2.5 py-1 transition-colors hover:bg-foreground/5"
|
||||
title={t("editor.project.renameTitle", "Rename project")}
|
||||
aria-label={t("editor.project.renameTitle", "Rename project")}
|
||||
>
|
||||
{project.hasUnsavedChanges ? (
|
||||
<span className="mt-[1px] size-2 shrink-0 rounded-full bg-[#2563EB]" />
|
||||
) : null}
|
||||
<span className="truncate text-sm font-semibold tracking-tight text-foreground/90">
|
||||
{projectDisplayName}
|
||||
</span>
|
||||
<span className="shrink-0 text-xs font-medium tracking-tight text-muted-foreground/70">
|
||||
.recordly
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center gap-2 justify-self-end pr-3"
|
||||
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
|
||||
>
|
||||
<Button
|
||||
ref={projectBrowserTriggerRef as React.Ref<HTMLButtonElement>}
|
||||
type="button"
|
||||
onClick={project.handleOpenProjectBrowser}
|
||||
className="inline-flex h-8 min-w-[96px] items-center justify-center gap-1.5 rounded-[5px] bg-neutral-800 px-4 text-white shadow-[0_14px_32px_rgba(0,0,0,0.18)] transition-colors hover:bg-neutral-700 dark:bg-white dark:text-black dark:hover:bg-white/90"
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
<span className="text-sm font-semibold tracking-tight">
|
||||
{t("editor.project.projects", "Projects")}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={project.handleSaveProject}
|
||||
className="inline-flex h-8 min-w-[96px] items-center justify-center gap-1.5 rounded-[5px] bg-neutral-800 px-4 text-white transition-colors hover:bg-neutral-700 dark:bg-white dark:text-black dark:hover:bg-white/90"
|
||||
>
|
||||
<span
|
||||
className={`${project.hasUnsavedChanges ? "flex" : "hidden"} size-2 relative`}
|
||||
>
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[#2563EB] opacity-75" />
|
||||
<span className="relative inline-flex size-2 rounded-full bg-[#2563EB]" />
|
||||
</span>
|
||||
<Save className="h-4 w-4" weight="fill" />
|
||||
<span className="text-sm font-semibold tracking-tight">
|
||||
{t("common.actions.save")}
|
||||
</span>
|
||||
</Button>
|
||||
<div className="mx-1 h-5 w-px bg-foreground/10" />
|
||||
<DropdownMenu
|
||||
open={exp.showExportDropdown}
|
||||
onOpenChange={exp.setShowExportDropdown}
|
||||
@@ -278,14 +347,17 @@ export function EditorHeader({
|
||||
<div
|
||||
className="h-full bg-[#2563EB] transition-all duration-300 ease-out"
|
||||
style={{
|
||||
width: `${Math.min(exp.isRenderingAudio ? ((exp.exportProgress as ExportProgress).audioProgress ?? 0) * 100 : (exp.exportFinalizingProgress ?? exp.exportProgress?.percentage ?? 8), 100)}%`,
|
||||
width: `${Math.min(
|
||||
exp.isRenderingAudio
|
||||
? ((exp.exportProgress as ExportProgress).audioProgress ?? 0) * 100
|
||||
: (exp.exportFinalizingProgress ?? exp.exportProgress?.percentage ?? 8),
|
||||
100,
|
||||
)}%`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
{exportPercentLabel}
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-muted-foreground">{exportPercentLabel}</p>
|
||||
{exp.isRenderingAudio ? (
|
||||
<p className="mt-1 text-[11px] text-muted-foreground/70">
|
||||
Audio requires real-time playback for speed/overlay edits
|
||||
@@ -340,7 +412,10 @@ export function EditorHeader({
|
||||
{t("editor.exportStatus.complete", "Export complete")}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{t("editor.exportStatus.savedSuccessfully", "Your file was saved successfully.")}
|
||||
{t(
|
||||
"editor.exportStatus.savedSuccessfully",
|
||||
"Your file was saved successfully.",
|
||||
)}
|
||||
</p>
|
||||
{exportRuntimeLabel ? (
|
||||
<p className="mt-1 text-[11px] text-muted-foreground/70">
|
||||
@@ -397,4 +472,4 @@ export function EditorHeader({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import ProjectBrowserDialog from "./ProjectBrowserDialog";
|
||||
import { fromFileUrl } from "./projectPersistence";
|
||||
import type { CropRegion } from "./types";
|
||||
import { VideoPlaybackRef } from "./VideoPlayback";
|
||||
import type { TimelineEditorHandle } from "./timeline/TimelineEditor";
|
||||
import TimelineEditor, { type TimelineEditorHandle } from "./timeline/TimelineEditor";
|
||||
import { getSmokeExportConfig } from "./videoEditorUtils";
|
||||
|
||||
export default function VideoEditor() {
|
||||
@@ -293,16 +293,6 @@ export default function VideoEditor() {
|
||||
}, [prefs.cropRegion]);
|
||||
|
||||
// ── Misc handlers ────────────────────────────────────────────────
|
||||
const openRecordingsFolder = useCallback(async () => {
|
||||
try {
|
||||
const result = await window.electronAPI.openRecordingsFolder();
|
||||
if (!result.success)
|
||||
toast.error(result.message || result.error || "Failed to open recordings folder.");
|
||||
} catch (err) {
|
||||
toast.error(`Failed to open recordings folder: ${String(err)}`);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const revealExportedFile = useCallback(async () => {
|
||||
if (!exp.exportedFilePath) return;
|
||||
try {
|
||||
@@ -368,12 +358,11 @@ export default function VideoEditor() {
|
||||
headerLeftControlsPaddingClass={headerLeftControlsPaddingClass}
|
||||
mp4OutputDimensions={wiring.mp4OutputDimensions}
|
||||
gifOutputDimensions={wiring.gifOutputDimensions}
|
||||
openRecordingsFolder={openRecordingsFolder}
|
||||
revealExportedFile={revealExportedFile}
|
||||
projectBrowserTriggerRef={projectBrowserTriggerRef}
|
||||
/>
|
||||
<div className="relative flex min-h-0 flex-1 flex-col gap-3 p-4">
|
||||
<div className="flex min-h-0 flex-1 gap-3">
|
||||
<div className="relative z-10 flex min-h-0 flex-1 gap-3">
|
||||
<EditorSidebar
|
||||
prefs={prefs}
|
||||
regions={regions}
|
||||
@@ -396,8 +385,6 @@ export default function VideoEditor() {
|
||||
isCropped={isCropped}
|
||||
hasSourceAudioFallback={hasSourceAudioFallback}
|
||||
effectiveCursorTelemetry={wiring.effectiveCursorTelemetry}
|
||||
normalizedCursorTelemetry={wiring.normalizedCursorTelemetry}
|
||||
autoSuggestZoomsTrigger={autoSuggestZoomsTrigger}
|
||||
videoPlaybackRef={videoPlaybackRef}
|
||||
timelineRef={timelineRef}
|
||||
setDuration={setDuration}
|
||||
@@ -411,7 +398,53 @@ export default function VideoEditor() {
|
||||
handleOpenCropEditor={handleOpenCropEditor}
|
||||
handleCloseCropEditor={handleCloseCropEditor}
|
||||
handleCancelCropEditor={handleCancelCropEditor}
|
||||
handleAutoSuggestZoomsConsumed={handleAutoSuggestZoomsConsumed}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="relative z-0 isolate flex-shrink-0 flex flex-col"
|
||||
style={{
|
||||
height: timelineCollapsed ? undefined : "15%",
|
||||
minHeight: timelineCollapsed ? 0 : 160,
|
||||
}}
|
||||
>
|
||||
<TimelineEditor
|
||||
ref={timelineRef}
|
||||
hideToolbar
|
||||
videoDuration={duration}
|
||||
currentTime={currentTime}
|
||||
playheadTime={regions.timelinePlayheadTime}
|
||||
onSeek={handleSeek}
|
||||
videoPath={videoPath}
|
||||
cursorTelemetry={wiring.normalizedCursorTelemetry}
|
||||
autoSuggestZoomsTrigger={autoSuggestZoomsTrigger}
|
||||
onAutoSuggestZoomsConsumed={handleAutoSuggestZoomsConsumed}
|
||||
zoomRegions={regions.zoomRegions}
|
||||
onZoomAdded={regions.handleZoomAdded}
|
||||
onZoomSuggested={regions.handleZoomSuggested}
|
||||
onZoomSpanChange={regions.handleZoomSpanChange}
|
||||
onZoomDelete={regions.handleZoomDelete}
|
||||
selectedZoomId={regions.selectedZoomId}
|
||||
onSelectZoom={regions.handleSelectZoom}
|
||||
trimRegions={regions.trimRegions}
|
||||
clipRegions={regions.clipRegions}
|
||||
onClipSplit={regions.handleClipSplit}
|
||||
onClipSpanChange={regions.handleClipSpanChange}
|
||||
onClipDelete={regions.handleClipDelete}
|
||||
selectedClipId={regions.selectedClipId}
|
||||
onSelectClip={regions.handleSelectClip}
|
||||
audioRegions={regions.audioRegions}
|
||||
onAudioAdded={regions.handleAudioAdded}
|
||||
onAudioSpanChange={regions.handleAudioSpanChange}
|
||||
onAudioDelete={regions.handleAudioDelete}
|
||||
selectedAudioId={regions.selectedAudioId}
|
||||
onSelectAudio={regions.handleSelectAudio}
|
||||
annotationRegions={regions.annotationRegions}
|
||||
onAnnotationAdded={regions.handleAnnotationAdded}
|
||||
onAnnotationSpanChange={regions.handleAnnotationSpanChange}
|
||||
onAnnotationDelete={regions.handleAnnotationDelete}
|
||||
selectedAnnotationId={regions.selectedAnnotationId}
|
||||
onSelectAnnotation={regions.handleSelectAnnotation}
|
||||
aspectRatio={prefs.aspectRatio}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,6 +39,14 @@ interface UseEditorProjectParams {
|
||||
clearHistory: () => void;
|
||||
}
|
||||
|
||||
type SaveProjectResult = {
|
||||
success: boolean;
|
||||
path?: string;
|
||||
message?: string;
|
||||
canceled?: boolean;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export function useEditorProject({
|
||||
getCurrentPersistedState,
|
||||
getCurrentSourcePath,
|
||||
@@ -71,18 +79,50 @@ export function useEditorProject({
|
||||
return JSON.stringify(current) !== JSON.stringify(lastSavedSnapshot);
|
||||
}, [getCurrentPersistedState, getCurrentSourcePath, getCurrentProjectPath, lastSavedSnapshot]);
|
||||
|
||||
const saveProject = useCallback(
|
||||
async (forceSaveAs: boolean) => {
|
||||
const sourcePath = getCurrentSourcePath();
|
||||
if (!sourcePath) {
|
||||
toast.error("No video loaded");
|
||||
const prepareProjectSave = useCallback(async () => {
|
||||
const sourcePath = getCurrentSourcePath();
|
||||
if (!sourcePath) {
|
||||
toast.error("No video loaded");
|
||||
return null;
|
||||
}
|
||||
|
||||
const projectData = createProjectData(sourcePath, getCurrentPersistedState());
|
||||
const fileNameBase =
|
||||
sourcePath.split(/[\\/]/).pop()?.replace(/\.[^.]+$/, "") ||
|
||||
`project-${Date.now()}`;
|
||||
const thumbnailDataUrl = await captureProjectThumbnail();
|
||||
|
||||
return {
|
||||
projectData,
|
||||
fileNameBase,
|
||||
thumbnailDataUrl,
|
||||
};
|
||||
}, [captureProjectThumbnail, getCurrentPersistedState, getCurrentSourcePath]);
|
||||
|
||||
const completeProjectSave = useCallback(
|
||||
async (result: SaveProjectResult, projectData: EditorProjectData) => {
|
||||
if (result.canceled) {
|
||||
toast.info("Project save canceled");
|
||||
return false;
|
||||
}
|
||||
if (!result.success) {
|
||||
toast.error(result.message || "Failed to save project");
|
||||
return false;
|
||||
}
|
||||
if (result.path) setCurrentProjectPath(result.path);
|
||||
setLastSavedSnapshot(globalThis.structuredClone(projectData));
|
||||
await refreshProjectLibrary();
|
||||
toast.success(result.path ? `Project saved to ${result.path}` : "Project saved");
|
||||
return true;
|
||||
},
|
||||
[refreshProjectLibrary, setCurrentProjectPath],
|
||||
);
|
||||
|
||||
const saveProject = useCallback(
|
||||
async (forceSaveAs: boolean) => {
|
||||
const preparedSave = await prepareProjectSave();
|
||||
if (!preparedSave) return false;
|
||||
try {
|
||||
const projectData = createProjectData(sourcePath, getCurrentPersistedState());
|
||||
const fileNameBase =
|
||||
sourcePath.split(/[\\/]/).pop()?.replace(/\.[^.]+$/, "") ||
|
||||
`project-${Date.now()}`;
|
||||
let targetProjectPath = forceSaveAs ? undefined : (getCurrentProjectPath() ?? undefined);
|
||||
|
||||
if (!forceSaveAs && !targetProjectPath) {
|
||||
@@ -93,42 +133,53 @@ export function useEditorProject({
|
||||
}
|
||||
}
|
||||
|
||||
const thumbnailDataUrl = await captureProjectThumbnail();
|
||||
const result = await window.electronAPI.saveProjectFile(
|
||||
projectData,
|
||||
fileNameBase,
|
||||
preparedSave.projectData,
|
||||
preparedSave.fileNameBase,
|
||||
targetProjectPath,
|
||||
thumbnailDataUrl,
|
||||
preparedSave.thumbnailDataUrl,
|
||||
);
|
||||
|
||||
if (result.canceled) {
|
||||
toast.info("Project save canceled");
|
||||
return false;
|
||||
}
|
||||
if (!result.success) {
|
||||
toast.error(result.message || "Failed to save project");
|
||||
return false;
|
||||
}
|
||||
if (result.path) setCurrentProjectPath(result.path);
|
||||
setLastSavedSnapshot(globalThis.structuredClone(projectData));
|
||||
await refreshProjectLibrary();
|
||||
toast.success(`Project saved to ${result.path}`);
|
||||
return true;
|
||||
return await completeProjectSave(result, preparedSave.projectData);
|
||||
} finally {
|
||||
remountPreview();
|
||||
}
|
||||
},
|
||||
[
|
||||
captureProjectThumbnail,
|
||||
getCurrentPersistedState,
|
||||
getCurrentSourcePath,
|
||||
completeProjectSave,
|
||||
getCurrentProjectPath,
|
||||
prepareProjectSave,
|
||||
setCurrentProjectPath,
|
||||
refreshProjectLibrary,
|
||||
remountPreview,
|
||||
],
|
||||
);
|
||||
|
||||
const saveProjectWithName = useCallback(
|
||||
async (projectName: string) => {
|
||||
const trimmedProjectName = projectName.trim();
|
||||
if (!trimmedProjectName) {
|
||||
toast.error("Project name is required");
|
||||
return false;
|
||||
}
|
||||
|
||||
const preparedSave = await prepareProjectSave();
|
||||
if (!preparedSave) return false;
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.saveProjectFileNamed(
|
||||
preparedSave.projectData,
|
||||
trimmedProjectName,
|
||||
preparedSave.thumbnailDataUrl,
|
||||
);
|
||||
|
||||
return await completeProjectSave(result, preparedSave.projectData);
|
||||
} finally {
|
||||
remountPreview();
|
||||
}
|
||||
},
|
||||
[completeProjectSave, prepareProjectSave, remountPreview],
|
||||
);
|
||||
|
||||
/** Load and apply a project from a raw (possibly unknown) candidate value. */
|
||||
const applyLoadedProject = useCallback(
|
||||
async (candidate: unknown, path?: string | null) => {
|
||||
@@ -218,6 +269,7 @@ export function useEditorProject({
|
||||
setLastSavedSnapshot,
|
||||
hasUnsavedChanges,
|
||||
saveProject,
|
||||
saveProjectWithName,
|
||||
handleSaveProject,
|
||||
handleSaveProjectAs,
|
||||
applyLoadedProject,
|
||||
|
||||
@@ -309,7 +309,7 @@ export function useEditorRegions({
|
||||
}) => {
|
||||
setZoomRegions(editor.zoomRegions);
|
||||
setClipRegions(editor.clipRegions);
|
||||
clipInitializedRef.current = true;
|
||||
clipInitializedRef.current = editor.clipRegions.length > 0;
|
||||
resetAnnotationAudioForProject(editor);
|
||||
setSelectedZoomId(null);
|
||||
setSelectedClipId(null);
|
||||
|
||||
@@ -76,6 +76,7 @@ export function useEditorWiring({
|
||||
(snapshot: EditorHistorySnapshot) => {
|
||||
regions.setZoomRegions(snapshot.zoomRegions);
|
||||
regions.setClipRegions(snapshot.clipRegions);
|
||||
regions.clipInitializedRef.current = snapshot.clipRegions.length > 0;
|
||||
regions.setAnnotationRegions(snapshot.annotationRegions);
|
||||
regions.setAudioRegions(snapshot.audioRegions);
|
||||
captions.setAutoCaptions(snapshot.autoCaptions);
|
||||
|
||||
@@ -221,8 +221,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(function
|
||||
videoReady,
|
||||
onTimeUpdate,
|
||||
onPlayStateChange,
|
||||
layoutVideoContent: layout.layoutVideoContent,
|
||||
updateOverlayForRegion: () => layout.updateOverlayForRegion(null),
|
||||
updateOverlayForRegion: layout.updateOverlayForRegion,
|
||||
});
|
||||
|
||||
usePlaybackTicker({
|
||||
|
||||
@@ -10,8 +10,7 @@ interface UsePixiVideoSceneParams {
|
||||
videoReady: boolean;
|
||||
onTimeUpdate: (time: number) => void;
|
||||
onPlayStateChange: (playing: boolean) => void;
|
||||
layoutVideoContent: () => void;
|
||||
updateOverlayForRegion: () => void;
|
||||
updateOverlayForRegion: (region: null) => void;
|
||||
}
|
||||
|
||||
export function usePixiVideoScene({
|
||||
@@ -20,7 +19,6 @@ export function usePixiVideoScene({
|
||||
videoReady,
|
||||
onTimeUpdate,
|
||||
onPlayStateChange,
|
||||
layoutVideoContent,
|
||||
updateOverlayForRegion,
|
||||
}: UsePixiVideoSceneParams) {
|
||||
useEffect(() => {
|
||||
@@ -64,7 +62,7 @@ export function usePixiVideoScene({
|
||||
refs.blurFilterRef.current = blurFilter;
|
||||
refs.motionBlurFilterRef.current = motionBlurFilter;
|
||||
|
||||
layoutVideoContent();
|
||||
refs.layoutVideoContentRef.current?.();
|
||||
video.pause();
|
||||
|
||||
const { handlePlay, handlePause, handleSeeked, handleSeeking } = createVideoEventHandlers({
|
||||
@@ -118,7 +116,7 @@ export function usePixiVideoScene({
|
||||
}
|
||||
videoTexture.destroy(false);
|
||||
refs.videoSpriteRef.current = null;
|
||||
updateOverlayForRegion();
|
||||
updateOverlayForRegion(null);
|
||||
};
|
||||
}, [layoutVideoContent, onPlayStateChange, onTimeUpdate, pixiReady, refs, updateOverlayForRegion, videoReady]);
|
||||
}, [onPlayStateChange, onTimeUpdate, pixiReady, refs, updateOverlayForRegion, videoReady]);
|
||||
}
|
||||
@@ -83,79 +83,156 @@ export function useVideoPlaybackRefs({
|
||||
"image" | "video" | "style"
|
||||
>("image");
|
||||
|
||||
const refs: VideoPlaybackRuntimeRefs = {
|
||||
videoRef: useRef<HTMLVideoElement | null>(null),
|
||||
containerRef: useRef<HTMLDivElement | null>(null),
|
||||
appRef: useRef<Application | null>(null),
|
||||
videoSpriteRef: useRef<Sprite | null>(null),
|
||||
videoContainerRef: useRef<Container | null>(null),
|
||||
cursorContainerRef: useRef<Container | null>(null),
|
||||
cameraContainerRef: useRef<Container | null>(null),
|
||||
timeUpdateAnimationRef: useRef<number | null>(null),
|
||||
overlayRef: useRef<HTMLDivElement | null>(null),
|
||||
focusIndicatorRef: useRef<HTMLDivElement | null>(null),
|
||||
webcamVideoRef: useRef<HTMLVideoElement | null>(null),
|
||||
webcamBubbleRef: useRef<HTMLDivElement | null>(null),
|
||||
webcamBubbleInnerRef: useRef<HTMLDivElement | null>(null),
|
||||
captionBoxRef: useRef<HTMLDivElement | null>(null),
|
||||
currentTimeRef: useRef(0),
|
||||
zoomRegionsRef: useRef(zoomRegions),
|
||||
selectedZoomIdRef: useRef<string | null>(selectedZoomId),
|
||||
animationStateRef: useRef(createPlaybackAnimationState()),
|
||||
blurFilterRef: useRef<BlurFilter | null>(null),
|
||||
motionBlurFilterRef: useRef<MotionBlurFilter | null>(null),
|
||||
isDraggingFocusRef: useRef(false),
|
||||
stageSizeRef: useRef({ width: 0, height: 0 }),
|
||||
videoSizeRef: useRef({ width: 0, height: 0 }),
|
||||
baseScaleRef: useRef(1),
|
||||
baseOffsetRef: useRef({ x: 0, y: 0 }),
|
||||
baseMaskRef: useRef({ x: 0, y: 0, width: 0, height: 0 }),
|
||||
cropBoundsRef: useRef({ startX: 0, endX: 0, startY: 0, endY: 0 }),
|
||||
maskGraphicsRef: useRef<Graphics | null>(null),
|
||||
frameSpriteRef: useRef<Sprite | null>(null),
|
||||
frameContainerRef: useRef<Container | null>(null),
|
||||
frameIdRef: useRef<string | null>(frame),
|
||||
isPlayingRef: useRef(isPlaying),
|
||||
isSeekingRef: useRef(false),
|
||||
allowPlaybackRef: useRef(false),
|
||||
lockedVideoDimensionsRef: useRef<{ width: number; height: number } | null>(null),
|
||||
layoutVideoContentRef: useRef<(() => void) | null>(null),
|
||||
trimRegionsRef: useRef<TrimRegion[]>(trimRegions),
|
||||
speedRegionsRef: useRef<SpeedRegion[]>(speedRegions),
|
||||
lastWebcamSyncTimeRef: useRef<number | null>(null),
|
||||
bgVideoRef: useRef<HTMLVideoElement | null>(null),
|
||||
zoomMotionBlurRef: useRef(zoomMotionBlur),
|
||||
connectZoomsRef: useRef(connectZooms),
|
||||
zoomInDurationMsRef: useRef(zoomInDurationMs),
|
||||
zoomInOverlapMsRef: useRef(zoomInOverlapMs),
|
||||
zoomOutDurationMsRef: useRef(zoomOutDurationMs),
|
||||
connectedZoomGapMsRef: useRef(connectedZoomGapMs),
|
||||
connectedZoomDurationMsRef: useRef(connectedZoomDurationMs),
|
||||
zoomInEasingRef: useRef(zoomInEasing),
|
||||
zoomOutEasingRef: useRef(zoomOutEasing),
|
||||
connectedZoomEasingRef: useRef(connectedZoomEasing),
|
||||
videoReadyRafRef: useRef<number | null>(null),
|
||||
cursorOverlayRef: useRef(null),
|
||||
cursorEffectsCanvasRef: useRef<HTMLCanvasElement | null>(null),
|
||||
cursorTelemetryRef: useRef(cursorTelemetry),
|
||||
showCursorRef: useRef(showCursor),
|
||||
cursorSizeRef: useRef(cursorSize),
|
||||
cursorStyleRef: useRef(cursorStyle),
|
||||
cursorSmoothingRef: useRef(cursorSmoothing),
|
||||
cursorMotionBlurRef: useRef(cursorMotionBlur),
|
||||
cursorClickBounceRef: useRef(cursorClickBounce),
|
||||
cursorClickBounceDurationRef: useRef(cursorClickBounceDuration),
|
||||
cursorSwayRef: useRef(cursorSway),
|
||||
lastEmittedClickTimeMsRef: useRef(-1),
|
||||
springScaleRef: useRef<SpringState>(createSpringState(1)),
|
||||
springXRef: useRef<SpringState>(createSpringState(0)),
|
||||
springYRef: useRef<SpringState>(createSpringState(0)),
|
||||
lastTickTimeRef: useRef<number | null>(null),
|
||||
zoomSmoothnessRef: useRef(zoomSmoothness),
|
||||
zoomClassicModeRef: useRef(zoomClassicMode),
|
||||
cursorFollowCameraRef: useRef<CursorFollowCameraState>(createCursorFollowCameraState()),
|
||||
motionBlurStateRef: useRef<MotionBlurState>(createMotionBlurState()),
|
||||
};
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const appRef = useRef<Application | null>(null);
|
||||
const videoSpriteRef = useRef<Sprite | null>(null);
|
||||
const videoContainerRef = useRef<Container | null>(null);
|
||||
const cursorContainerRef = useRef<Container | null>(null);
|
||||
const cameraContainerRef = useRef<Container | null>(null);
|
||||
const timeUpdateAnimationRef = useRef<number | null>(null);
|
||||
const overlayRef = useRef<HTMLDivElement | null>(null);
|
||||
const focusIndicatorRef = useRef<HTMLDivElement | null>(null);
|
||||
const webcamVideoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const webcamBubbleRef = useRef<HTMLDivElement | null>(null);
|
||||
const webcamBubbleInnerRef = useRef<HTMLDivElement | null>(null);
|
||||
const captionBoxRef = useRef<HTMLDivElement | null>(null);
|
||||
const currentTimeRef = useRef(0);
|
||||
const zoomRegionsRef = useRef(zoomRegions);
|
||||
const selectedZoomIdRef = useRef<string | null>(selectedZoomId);
|
||||
const animationStateRef = useRef(createPlaybackAnimationState());
|
||||
const blurFilterRef = useRef<BlurFilter | null>(null);
|
||||
const motionBlurFilterRef = useRef<MotionBlurFilter | null>(null);
|
||||
const isDraggingFocusRef = useRef(false);
|
||||
const stageSizeRef = useRef({ width: 0, height: 0 });
|
||||
const videoSizeRef = useRef({ width: 0, height: 0 });
|
||||
const baseScaleRef = useRef(1);
|
||||
const baseOffsetRef = useRef({ x: 0, y: 0 });
|
||||
const baseMaskRef = useRef({ x: 0, y: 0, width: 0, height: 0 });
|
||||
const cropBoundsRef = useRef({ startX: 0, endX: 0, startY: 0, endY: 0 });
|
||||
const maskGraphicsRef = useRef<Graphics | null>(null);
|
||||
const frameSpriteRef = useRef<Sprite | null>(null);
|
||||
const frameContainerRef = useRef<Container | null>(null);
|
||||
const frameIdRef = useRef<string | null>(frame);
|
||||
const isPlayingRef = useRef(isPlaying);
|
||||
const isSeekingRef = useRef(false);
|
||||
const allowPlaybackRef = useRef(false);
|
||||
const lockedVideoDimensionsRef = useRef<{ width: number; height: number } | null>(null);
|
||||
const layoutVideoContentRef = useRef<(() => void) | null>(null);
|
||||
const trimRegionsRef = useRef<TrimRegion[]>(trimRegions);
|
||||
const speedRegionsRef = useRef<SpeedRegion[]>(speedRegions);
|
||||
const lastWebcamSyncTimeRef = useRef<number | null>(null);
|
||||
const bgVideoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const zoomMotionBlurRef = useRef(zoomMotionBlur);
|
||||
const connectZoomsRef = useRef(connectZooms);
|
||||
const zoomInDurationMsRef = useRef(zoomInDurationMs);
|
||||
const zoomInOverlapMsRef = useRef(zoomInOverlapMs);
|
||||
const zoomOutDurationMsRef = useRef(zoomOutDurationMs);
|
||||
const connectedZoomGapMsRef = useRef(connectedZoomGapMs);
|
||||
const connectedZoomDurationMsRef = useRef(connectedZoomDurationMs);
|
||||
const zoomInEasingRef = useRef(zoomInEasing);
|
||||
const zoomOutEasingRef = useRef(zoomOutEasing);
|
||||
const connectedZoomEasingRef = useRef(connectedZoomEasing);
|
||||
const videoReadyRafRef = useRef<number | null>(null);
|
||||
const cursorOverlayRef = useRef(null);
|
||||
const cursorEffectsCanvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const cursorTelemetryRef = useRef(cursorTelemetry);
|
||||
const showCursorRef = useRef(showCursor);
|
||||
const cursorSizeRef = useRef(cursorSize);
|
||||
const cursorStyleRef = useRef(cursorStyle);
|
||||
const cursorSmoothingRef = useRef(cursorSmoothing);
|
||||
const cursorMotionBlurRef = useRef(cursorMotionBlur);
|
||||
const cursorClickBounceRef = useRef(cursorClickBounce);
|
||||
const cursorClickBounceDurationRef = useRef(cursorClickBounceDuration);
|
||||
const cursorSwayRef = useRef(cursorSway);
|
||||
const lastEmittedClickTimeMsRef = useRef(-1);
|
||||
const springScaleRef = useRef<SpringState>(createSpringState(1));
|
||||
const springXRef = useRef<SpringState>(createSpringState(0));
|
||||
const springYRef = useRef<SpringState>(createSpringState(0));
|
||||
const lastTickTimeRef = useRef<number | null>(null);
|
||||
const zoomSmoothnessRef = useRef(zoomSmoothness);
|
||||
const zoomClassicModeRef = useRef(zoomClassicMode);
|
||||
const cursorFollowCameraRef = useRef<CursorFollowCameraState>(createCursorFollowCameraState());
|
||||
const motionBlurStateRef = useRef<MotionBlurState>(createMotionBlurState());
|
||||
|
||||
const refsRef = useRef<VideoPlaybackRuntimeRefs | null>(null);
|
||||
if (!refsRef.current) {
|
||||
refsRef.current = {
|
||||
videoRef,
|
||||
containerRef,
|
||||
appRef,
|
||||
videoSpriteRef,
|
||||
videoContainerRef,
|
||||
cursorContainerRef,
|
||||
cameraContainerRef,
|
||||
timeUpdateAnimationRef,
|
||||
overlayRef,
|
||||
focusIndicatorRef,
|
||||
webcamVideoRef,
|
||||
webcamBubbleRef,
|
||||
webcamBubbleInnerRef,
|
||||
captionBoxRef,
|
||||
currentTimeRef,
|
||||
zoomRegionsRef,
|
||||
selectedZoomIdRef,
|
||||
animationStateRef,
|
||||
blurFilterRef,
|
||||
motionBlurFilterRef,
|
||||
isDraggingFocusRef,
|
||||
stageSizeRef,
|
||||
videoSizeRef,
|
||||
baseScaleRef,
|
||||
baseOffsetRef,
|
||||
baseMaskRef,
|
||||
cropBoundsRef,
|
||||
maskGraphicsRef,
|
||||
frameSpriteRef,
|
||||
frameContainerRef,
|
||||
frameIdRef,
|
||||
isPlayingRef,
|
||||
isSeekingRef,
|
||||
allowPlaybackRef,
|
||||
lockedVideoDimensionsRef,
|
||||
layoutVideoContentRef,
|
||||
trimRegionsRef,
|
||||
speedRegionsRef,
|
||||
lastWebcamSyncTimeRef,
|
||||
bgVideoRef,
|
||||
zoomMotionBlurRef,
|
||||
connectZoomsRef,
|
||||
zoomInDurationMsRef,
|
||||
zoomInOverlapMsRef,
|
||||
zoomOutDurationMsRef,
|
||||
connectedZoomGapMsRef,
|
||||
connectedZoomDurationMsRef,
|
||||
zoomInEasingRef,
|
||||
zoomOutEasingRef,
|
||||
connectedZoomEasingRef,
|
||||
videoReadyRafRef,
|
||||
cursorOverlayRef,
|
||||
cursorEffectsCanvasRef,
|
||||
cursorTelemetryRef,
|
||||
showCursorRef,
|
||||
cursorSizeRef,
|
||||
cursorStyleRef,
|
||||
cursorSmoothingRef,
|
||||
cursorMotionBlurRef,
|
||||
cursorClickBounceRef,
|
||||
cursorClickBounceDurationRef,
|
||||
cursorSwayRef,
|
||||
lastEmittedClickTimeMsRef,
|
||||
springScaleRef,
|
||||
springXRef,
|
||||
springYRef,
|
||||
lastTickTimeRef,
|
||||
zoomSmoothnessRef,
|
||||
zoomClassicModeRef,
|
||||
cursorFollowCameraRef,
|
||||
motionBlurStateRef,
|
||||
};
|
||||
}
|
||||
|
||||
const refs = refsRef.current;
|
||||
|
||||
return {
|
||||
refs,
|
||||
|
||||
@@ -296,6 +296,7 @@ export function useVideoPlaybackSync({
|
||||
const videoStage = refs.videoContainerRef.current;
|
||||
const sprite = refs.videoSpriteRef.current;
|
||||
const currentApp = refs.appRef.current;
|
||||
const layout = refs.layoutVideoContentRef.current;
|
||||
if (!container || !videoStage || !sprite || !currentApp) return;
|
||||
|
||||
container.scale.set(1);
|
||||
@@ -305,7 +306,7 @@ export function useVideoPlaybackSync({
|
||||
sprite.scale.set(1);
|
||||
sprite.position.set(0, 0);
|
||||
|
||||
layoutVideoContent();
|
||||
layout?.();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const finalApp = refs.appRef.current;
|
||||
@@ -317,7 +318,12 @@ export function useVideoPlaybackSync({
|
||||
}
|
||||
});
|
||||
});
|
||||
}, [layoutVideoContent, pixiReady, refs, videoReady]);
|
||||
}, [pixiReady, refs, videoPath, videoReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pixiReady || !videoReady) return;
|
||||
layoutVideoContent();
|
||||
}, [layoutVideoContent, pixiReady, videoReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pixiReady || !videoReady) return;
|
||||
|
||||
Reference in New Issue
Block a user