Vendored Augani/openreel-video (MIT) into services/editor and wired it to the MAM. Editor runs as its own container on port 47435. Library assets pull in via ?asset=<uuid>; render exports route back via POST /api/v1/upload/simple. Sidebar Editor link on every page; Edit button on every preview modal. See services/editor/INTEGRATION.md for the patch map.
170 lines
5.6 KiB
TypeScript
170 lines
5.6 KiB
TypeScript
import { useEffect, useCallback, useRef, lazy, Suspense } from "react";
|
|
import { ToastContainer } from "./components/Toast";
|
|
import { ScriptViewDialog } from "./components/editor/ScriptViewDialog";
|
|
import { SearchModal } from "./components/editor/SearchModal";
|
|
import { MobileBlocker } from "./components/MobileBlocker";
|
|
import { WelcomeScreen } from "./components/welcome";
|
|
import { RecoveryDialog } from "./components/welcome/RecoveryDialog";
|
|
import { SharePage } from "./pages/SharePage";
|
|
import { useUIStore } from "./stores/ui-store";
|
|
import { useProjectStore } from "./stores/project-store";
|
|
import { useRouter } from "./hooks/use-router";
|
|
import { useProjectRecovery } from "./hooks/useProjectRecovery";
|
|
import { useKieAIPoller } from "./hooks/useKieAIPoller";
|
|
import { SOCIAL_MEDIA_PRESETS, type SocialMediaCategory } from "@openreel/core";
|
|
import { TooltipProvider } from "@openreel/ui";
|
|
|
|
const EditorInterface = lazy(() =>
|
|
import("./components/editor/EditorInterface").then((m) => ({
|
|
default: m.EditorInterface,
|
|
}))
|
|
);
|
|
|
|
const LoadingSpinner: React.FC<{ message: string }> = ({ message }) => (
|
|
<div className="h-screen w-screen bg-background flex flex-col items-center justify-center">
|
|
<div className="w-10 h-10 border-2 border-primary border-t-transparent rounded-full animate-spin mb-3" />
|
|
<p className="text-sm text-text-secondary">{message}</p>
|
|
</div>
|
|
);
|
|
|
|
const PRESET_DIMENSIONS: Record<string, SocialMediaCategory> = {
|
|
"1080x1920": "tiktok",
|
|
"1920x1080": "youtube-video",
|
|
"1080x1080": "instagram-post",
|
|
"720x1280": "instagram-stories",
|
|
"1280x720": "youtube-video",
|
|
};
|
|
|
|
function App() {
|
|
const { activeModal, closeModal, skipWelcomeScreen } = useUIStore();
|
|
const { openModal: openSearchModal } = useUIStore();
|
|
const createNewProject = useProjectStore((state) => state.createNewProject);
|
|
const { showDialog, availableSaves, recover, dismiss, clearAll } = useProjectRecovery();
|
|
|
|
const { route, params, navigate, parsedDimensions, fps } = useRouter();
|
|
const hasHandledInitialRoute = useRef(false);
|
|
|
|
useKieAIPoller();
|
|
|
|
useEffect(() => {
|
|
if (hasHandledInitialRoute.current) return;
|
|
|
|
if (route === "new") {
|
|
hasHandledInitialRoute.current = true;
|
|
|
|
let projectName = "New Project";
|
|
let width = 1920;
|
|
let height = 1080;
|
|
let frameRate = fps;
|
|
|
|
if (params.preset) {
|
|
const presetKey = params.preset as SocialMediaCategory;
|
|
const preset = SOCIAL_MEDIA_PRESETS[presetKey];
|
|
if (preset) {
|
|
width = preset.width;
|
|
height = preset.height;
|
|
frameRate = preset.frameRate || fps;
|
|
projectName = `New ${presetKey.charAt(0).toUpperCase() + presetKey.slice(1).replace(/-/g, " ")} Project`;
|
|
}
|
|
} else if (parsedDimensions) {
|
|
width = parsedDimensions.width;
|
|
height = parsedDimensions.height;
|
|
|
|
const dimensionKey = `${width}x${height}`;
|
|
const matchingPreset = PRESET_DIMENSIONS[dimensionKey];
|
|
if (matchingPreset) {
|
|
const preset = SOCIAL_MEDIA_PRESETS[matchingPreset];
|
|
frameRate = preset.frameRate || fps;
|
|
}
|
|
|
|
const aspectRatio = width / height;
|
|
if (aspectRatio < 1) {
|
|
projectName = "New Vertical Video";
|
|
} else if (aspectRatio > 1) {
|
|
projectName = "New Horizontal Video";
|
|
} else {
|
|
projectName = "New Square Video";
|
|
}
|
|
}
|
|
|
|
createNewProject(projectName, { width, height, frameRate });
|
|
navigate("editor");
|
|
} else if (route === "editor" && skipWelcomeScreen) {
|
|
hasHandledInitialRoute.current = true;
|
|
} else if (["welcome", "templates", "recent"].includes(route)) {
|
|
hasHandledInitialRoute.current = true;
|
|
}
|
|
}, [
|
|
route,
|
|
params,
|
|
parsedDimensions,
|
|
fps,
|
|
createNewProject,
|
|
navigate,
|
|
skipWelcomeScreen,
|
|
]);
|
|
|
|
const handleKeyDown = useCallback(
|
|
(e: KeyboardEvent) => {
|
|
if (e.key === "Escape" && route !== "editor") {
|
|
navigate("editor");
|
|
}
|
|
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
|
e.preventDefault();
|
|
openSearchModal("search");
|
|
}
|
|
},
|
|
[route, navigate, openSearchModal],
|
|
);
|
|
|
|
useEffect(() => {
|
|
window.addEventListener("keydown", handleKeyDown);
|
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
}, [handleKeyDown]);
|
|
|
|
const showWelcome =
|
|
["welcome", "templates", "recent"].includes(route) && !skipWelcomeScreen;
|
|
const initialTab =
|
|
route === "templates"
|
|
? "templates"
|
|
: route === "recent"
|
|
? "recent"
|
|
: undefined;
|
|
const isSharePage = route === "share" && params.shareId;
|
|
|
|
return (
|
|
<TooltipProvider>
|
|
<div className="h-screen w-screen bg-background text-text-primary overflow-hidden">
|
|
<MobileBlocker />
|
|
{isSharePage ? (
|
|
<SharePage shareId={params.shareId!} />
|
|
) : showWelcome ? (
|
|
<WelcomeScreen initialTab={initialTab} />
|
|
) : (
|
|
<Suspense fallback={<LoadingSpinner message="Loading editor..." />}>
|
|
<EditorInterface />
|
|
</Suspense>
|
|
)}
|
|
<ToastContainer />
|
|
<ScriptViewDialog
|
|
isOpen={activeModal === "scriptView"}
|
|
onClose={closeModal}
|
|
/>
|
|
<SearchModal isOpen={activeModal === "search"} onClose={closeModal} />
|
|
{showDialog && availableSaves.length > 0 && (
|
|
<RecoveryDialog
|
|
saves={availableSaves}
|
|
onRecover={async (saveId) => {
|
|
const success = await recover(saveId);
|
|
if (success) navigate("editor");
|
|
}}
|
|
onDismiss={dismiss}
|
|
onClearAll={clearAll}
|
|
/>
|
|
)}
|
|
</div>
|
|
</TooltipProvider>
|
|
);
|
|
}
|
|
|
|
export default App;
|