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.
185 lines
4.2 KiB
TypeScript
185 lines
4.2 KiB
TypeScript
import { useState, useEffect, useCallback, useMemo } from "react";
|
|
|
|
export type AppRoute =
|
|
| "welcome"
|
|
| "editor"
|
|
| "new"
|
|
| "templates"
|
|
| "recent"
|
|
| "share";
|
|
|
|
export interface RouteParams {
|
|
dimensions?: string;
|
|
preset?: string;
|
|
width?: string;
|
|
height?: string;
|
|
fps?: string;
|
|
tab?: string;
|
|
shareId?: string;
|
|
}
|
|
|
|
export interface RouterState {
|
|
route: AppRoute;
|
|
params: RouteParams;
|
|
}
|
|
|
|
function parseHash(hash: string): RouterState {
|
|
const cleanHash = hash.replace(/^#\/?/, "");
|
|
const [path, queryString] = cleanHash.split("?");
|
|
|
|
const params: RouteParams = {};
|
|
if (queryString) {
|
|
const searchParams = new URLSearchParams(queryString);
|
|
searchParams.forEach((value, key) => {
|
|
params[key as keyof RouteParams] = value;
|
|
});
|
|
}
|
|
|
|
const pathParts = path.split("/");
|
|
let route: AppRoute = (pathParts[0] || "welcome") as AppRoute;
|
|
const validRoutes: AppRoute[] = [
|
|
"welcome",
|
|
"editor",
|
|
"new",
|
|
"templates",
|
|
"recent",
|
|
"share",
|
|
];
|
|
|
|
if (route === "share" && pathParts[1]) {
|
|
params.shareId = pathParts[1];
|
|
}
|
|
|
|
return {
|
|
route: validRoutes.includes(route) ? route : "welcome",
|
|
params,
|
|
};
|
|
}
|
|
|
|
function buildHash(route: AppRoute, params?: RouteParams): string {
|
|
let hash = `#/${route}`;
|
|
|
|
if (params && Object.keys(params).length > 0) {
|
|
const searchParams = new URLSearchParams();
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
if (value !== undefined && value !== null) {
|
|
searchParams.set(key, String(value));
|
|
}
|
|
});
|
|
const queryString = searchParams.toString();
|
|
if (queryString) {
|
|
hash += `?${queryString}`;
|
|
}
|
|
}
|
|
|
|
return hash;
|
|
}
|
|
|
|
export function useRouter() {
|
|
const [state, setState] = useState<RouterState>(() => {
|
|
if (typeof window !== "undefined") {
|
|
return parseHash(window.location.hash);
|
|
}
|
|
return { route: "welcome", params: {} };
|
|
});
|
|
|
|
useEffect(() => {
|
|
const handleHashChange = () => {
|
|
setState(parseHash(window.location.hash));
|
|
};
|
|
|
|
window.addEventListener("hashchange", handleHashChange);
|
|
return () => window.removeEventListener("hashchange", handleHashChange);
|
|
}, []);
|
|
|
|
const navigate = useCallback((route: AppRoute, params?: RouteParams) => {
|
|
const hash = buildHash(route, params);
|
|
window.location.hash = hash;
|
|
}, []);
|
|
|
|
const updateParams = useCallback(
|
|
(newParams: Partial<RouteParams>) => {
|
|
const hash = buildHash(state.route, { ...state.params, ...newParams });
|
|
window.location.hash = hash;
|
|
},
|
|
[state.route, state.params],
|
|
);
|
|
|
|
const clearParams = useCallback(() => {
|
|
const hash = buildHash(state.route);
|
|
window.location.hash = hash;
|
|
}, [state.route]);
|
|
|
|
const parsedDimensions = useMemo(() => {
|
|
const { dimensions, width, height } = state.params;
|
|
|
|
if (dimensions) {
|
|
const match = dimensions.match(/^(\d+)x(\d+)$/i);
|
|
if (match) {
|
|
return {
|
|
width: parseInt(match[1], 10),
|
|
height: parseInt(match[2], 10),
|
|
};
|
|
}
|
|
}
|
|
|
|
if (width && height) {
|
|
return { width: parseInt(width, 10), height: parseInt(height, 10) };
|
|
}
|
|
|
|
return null;
|
|
}, [state.params]);
|
|
|
|
const fps = useMemo(() => {
|
|
const { fps } = state.params;
|
|
if (fps) {
|
|
const parsed = parseInt(fps, 10);
|
|
if (!isNaN(parsed) && parsed > 0 && parsed <= 120) {
|
|
return parsed;
|
|
}
|
|
}
|
|
return 30;
|
|
}, [state.params]);
|
|
|
|
return {
|
|
route: state.route,
|
|
params: state.params,
|
|
navigate,
|
|
updateParams,
|
|
clearParams,
|
|
parsedDimensions,
|
|
fps,
|
|
};
|
|
}
|
|
|
|
export function generateShareableLink(
|
|
route: AppRoute,
|
|
params?: RouteParams,
|
|
): string {
|
|
const baseUrl =
|
|
typeof window !== "undefined"
|
|
? `${window.location.origin}${window.location.pathname}`
|
|
: "";
|
|
return `${baseUrl}${buildHash(route, params)}`;
|
|
}
|
|
|
|
export function generateNewProjectLink(options: {
|
|
width?: number;
|
|
height?: number;
|
|
preset?: string;
|
|
fps?: number;
|
|
}): string {
|
|
const params: RouteParams = {};
|
|
|
|
if (options.preset) {
|
|
params.preset = options.preset;
|
|
} else if (options.width && options.height) {
|
|
params.dimensions = `${options.width}x${options.height}`;
|
|
}
|
|
|
|
if (options.fps && options.fps !== 30) {
|
|
params.fps = String(options.fps);
|
|
}
|
|
|
|
return generateShareableLink("new", params);
|
|
}
|