Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions packages/studio/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { buildProjectApiPath } from "./utils/projectRouting";
import { useState, useCallback, useRef, useMemo, useLayoutEffect } from "react";
import type { LeftSidebarHandle, SidebarTab } from "./components/sidebar/LeftSidebar";
import { useRenderQueue } from "./components/renders/useRenderQueue";
Expand Down Expand Up @@ -326,9 +327,10 @@ export function StudioApp() {
const renderClipContent = useRenderClipContent({
projectIdRef: fileManager.projectIdRef,
compIdToSrc,
activePreviewUrl: activeCompPath
? `/api/projects/${projectId}/preview/comp/${activeCompPath}`
: null,
activePreviewUrl:
activeCompPath && projectId
? buildProjectApiPath(projectId, `/preview/comp/${activeCompPath}`)
: null,
effectiveTimelineDuration,
});
const compositionDimensions = useCompositionDimensions();
Expand Down
5 changes: 3 additions & 2 deletions packages/studio/src/captions/hooks/useCaptionSync.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { buildProjectApiPath } from "../../utils/projectRouting";
import { useCallback, useRef } from "react";
import { useCaptionStore } from "../store";
import { useMountEffect } from "../../hooks/useMountEffect";
Expand Down Expand Up @@ -92,7 +93,7 @@ export function useCaptionSync(projectId: string | null) {
const seqAtSave = editSeqRef.current;
const overrides = buildOverrides(state.model);

fetch(`/api/projects/${pid}/files/${encodeURIComponent("caption-overrides.json")}`, {
fetch(buildProjectApiPath(pid, `/files/${encodeURIComponent("caption-overrides.json")}`), {
method: "PUT",
headers: { "Content-Type": "text/plain", ...studioWriteHeaders() },
body: JSON.stringify(overrides, null, 2),
Expand Down Expand Up @@ -171,7 +172,7 @@ export function useCaptionSync(projectId: string | null) {
let data: { content?: string };
try {
const res = await fetch(
`/api/projects/${pid}/files/${encodeURIComponent("caption-overrides.json")}`,
buildProjectApiPath(pid, `/files/${encodeURIComponent("caption-overrides.json")}`),
);
if (!res.ok) return; // no overrides file yet — normal
data = await res.json();
Expand Down
3 changes: 2 additions & 1 deletion packages/studio/src/components/MediaPreview.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { buildProjectApiPath } from "../utils/projectRouting";
import { useState } from "react";
import { IMAGE_EXT, VIDEO_EXT, AUDIO_EXT } from "../utils/mediaTypes";

Expand Down Expand Up @@ -28,7 +29,7 @@ function MediaErrorPanel({ name, filePath }: { name: string; filePath: string })
}

export function MediaPreview({ projectId, filePath }: { projectId: string; filePath: string }) {
const serveUrl = `/api/projects/${projectId}/preview/${filePath}`;
const serveUrl = buildProjectApiPath(projectId, `/preview/${filePath}`);
const name = filePath.split("/").pop() ?? filePath;
// Keyed by path so switching to another file clears a previous failure.
const [failedPath, setFailedPath] = useState<string | null>(null);
Expand Down
26 changes: 1 addition & 25 deletions packages/studio/src/components/editor/domEditingLayers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { probeSourceElement } from "./probeSourceElement";
import type { PatchOperation } from "../../utils/sourcePatcher";
import {
resolveEditingAffordances,
Expand Down Expand Up @@ -281,31 +282,6 @@ export function resolveDomEditCapabilities(args: {
).capabilities;
}

async function probeSourceElement(
projectId: string,
sourceFile: string,
target: { id?: string; hfId?: string; selector?: string; selectorIndex?: number },
): Promise<boolean> {
try {
const response = await fetch(
`/api/projects/${projectId}/file-mutations/probe-element/${encodeURIComponent(sourceFile)}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ target }),
},
);
if (!response.ok) return true;
const data = await response.json();
if (data && typeof data === "object" && "exists" in data && data.exists === false) {
return false;
}
return true;
} catch {
return true;
}
}

// fallow-ignore-next-line complexity
export async function resolveDomEditSelection(
startEl: HTMLElement | null,
Expand Down
29 changes: 29 additions & 0 deletions packages/studio/src/components/editor/probeSourceElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { buildProjectApiPath } from "../../utils/projectRouting";

export async function probeSourceElement(
projectId: string,
sourceFile: string,
target: { id?: string; hfId?: string; selector?: string; selectorIndex?: number },
): Promise<boolean> {
try {
const response = await fetch(
buildProjectApiPath(
projectId,
`/file-mutations/probe-element/${encodeURIComponent(sourceFile)}`,
),
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ target }),
},
);
if (!response.ok) return true;
const data = await response.json();
if (data && typeof data === "object" && "exists" in data && data.exists === false) {
return false;
}
return true;
} catch {
return true;
}
}
3 changes: 2 additions & 1 deletion packages/studio/src/components/editor/propertyPanelFill.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { buildProjectApiPath } from "../../utils/projectRouting";
import { useMemo, useRef, useState } from "react";
import { Plus, RotateCcw, X } from "../../icons/SystemIcons";
import {
Expand Down Expand Up @@ -158,7 +159,7 @@ export function ImageFillField({
{selectedAsset && (
<div className="overflow-hidden rounded-xl border border-neutral-800 bg-neutral-900/80">
<img
src={`/api/projects/${projectId}/preview/${selectedAsset}`}
src={buildProjectApiPath(projectId, `/preview/${selectedAsset}`)}
alt={selectedAsset.split("/").pop() ?? selectedAsset}
className="h-28 w-full object-contain bg-neutral-950/80"
/>
Expand Down
5 changes: 4 additions & 1 deletion packages/studio/src/components/feedback/projectProvenance.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { buildProjectApiPath } from "../../utils/projectRouting";
// ---------------------------------------------------------------------------
// How this project came to exist, and roughly what shape it is.
//
Expand Down Expand Up @@ -53,7 +54,9 @@ export async function captureProjectProvenance(
if (!scaffolded) return;

try {
const res = await fetch(`/api/projects/${projectId}/files/${encodeURIComponent(CONFIG_FILE)}`);
const res = await fetch(
buildProjectApiPath(projectId, `/files/${encodeURIComponent(CONFIG_FILE)}`),
);
if (!res.ok) return;
// The route answers with an envelope, not the file: {filename, content,
// version}. The config is the `content` string inside it.
Expand Down
3 changes: 2 additions & 1 deletion packages/studio/src/components/nle/NLEContext.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { buildProjectApiPath } from "../../utils/projectRouting";
import { useContext, useState, useCallback, useRef, useEffect, type ReactNode } from "react";
import { useTimelinePlayer, usePlayerStore } from "../../player";
import type { TimelineElement } from "../../player";
Expand Down Expand Up @@ -185,7 +186,7 @@ export function NLEProvider({
setCompositionSourceMap(emptyMap);
onCompIdToSrcChangeRef.current?.(emptyMap);

fetch(`/api/projects/${projectId}/files/index.html`, {
fetch(buildProjectApiPath(projectId, `/files/index.html`), {
signal: controller.signal,
})
.then((r) => {
Expand Down
15 changes: 11 additions & 4 deletions packages/studio/src/components/nle/useCompositionStack.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { buildProjectApiPath } from "../../utils/projectRouting";
// Composition drill-down stack management for NLEContext/EditorShell
import { useState, useCallback, useRef, useEffect } from "react";
import { usePlayerStore } from "../../player";
Expand Down Expand Up @@ -29,7 +30,7 @@ export function useCompositionStack({
{
id: "master",
label: "Master",
previewUrl: `/api/projects/${projectId}/preview`,
previewUrl: buildProjectApiPath(projectId, `/preview`),
},
]);

Expand Down Expand Up @@ -89,7 +90,10 @@ export function useCompositionStack({
.split("/")
.pop()
?.replace(/\.html$/, "") || resolvedPath;
const previewUrl = `/api/projects/${projectId}/preview/comp/${encodePreviewPath(resolvedPath)}`;
const previewUrl = buildProjectApiPath(
projectId,
`/preview/comp/${encodePreviewPath(resolvedPath)}`,
);
return [...prev, { id: resolvedPath, label, previewUrl }];
});
},
Expand All @@ -103,7 +107,7 @@ export function useCompositionStack({
const master: CompositionLevel = {
id: "master",
label: "Master",
previewUrl: `/api/projects/${projectId}/preview`,
previewUrl: buildProjectApiPath(projectId, `/preview`),
};
if (activeCompositionPath === "index.html") {
usePlayerStore.getState().setElements([]);
Expand All @@ -116,7 +120,10 @@ export function useCompositionStack({
// panel highlighted the row, so the canvas and timeline stayed on
// index.html and edits landed in the root file.
const label = activeCompositionPath.replace(/^compositions\//, "").replace(/\.html$/, "");
const previewUrl = `/api/projects/${projectId}/preview/comp/${encodePreviewPath(activeCompositionPath)}`;
const previewUrl = buildProjectApiPath(
projectId,
`/preview/comp/${encodePreviewPath(activeCompositionPath)}`,
);
usePlayerStore.getState().setElements([]);
updateCompositionStack((prev) => {
if (prev[prev.length - 1]?.id === activeCompositionPath) return prev;
Expand Down
3 changes: 2 additions & 1 deletion packages/studio/src/components/renders/RenderQueueItem.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { buildProjectApiPath } from "../../utils/projectRouting";
import { memo, useCallback, useState } from "react";
import { VideoFrameThumbnail } from "../ui/VideoFrameThumbnail";
import { Button } from "../ui/Button";
Expand Down Expand Up @@ -36,7 +37,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
const [confirmingDelete, setConfirmingDelete] = useState(false);

// Direct file URL — serves from disk, survives server restarts
const fileSrc = `/api/projects/${projectId}/renders/file/${job.filename}`;
const fileSrc = buildProjectApiPath(projectId, `/renders/file/${job.filename}`);

const handleOpen = useCallback(() => {
window.open(fileSrc, "_blank");
Expand Down
5 changes: 3 additions & 2 deletions packages/studio/src/components/renders/useRenderQueue.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { buildProjectApiPath } from "../../utils/projectRouting";
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
import type { CanvasResolution } from "@hyperframes/parsers";
import { trackStudioRenderStart } from "../../telemetry/events";
Expand Down Expand Up @@ -125,7 +126,7 @@ export function useRenderQueue(
const loadRenders = useCallback(async () => {
if (!projectId) return;
try {
const res = await fetch(`/api/projects/${projectId}/renders`);
const res = await fetch(buildProjectApiPath(projectId, `/renders`));
if (!res.ok) {
setLoadError(`Couldn't load render history (server error ${res.status}).`);
return;
Expand Down Expand Up @@ -261,7 +262,7 @@ export function useRenderQueue(
}
let res: Response;
try {
res = await fetch(`/api/projects/${projectId}/render`, {
res = await fetch(buildProjectApiPath(projectId, `/render`), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
Expand Down
3 changes: 2 additions & 1 deletion packages/studio/src/components/sidebar/AssetsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { buildProjectApiPath } from "../../utils/projectRouting";
// fallow-ignore-file code-duplication
import { memo, useState, useCallback, useRef, useMemo, useEffect } from "react";
import { SearchInput } from "../ui/SearchInput";
Expand Down Expand Up @@ -216,7 +217,7 @@ export const AssetsTab = memo(function AssetsTab({
useEffect(() => {
if (manifest404Ref.current.has(projectId)) return;
let cancelled = false;
fetch(`/api/projects/${projectId}/preview/.media/manifest.jsonl`)
fetch(buildProjectApiPath(projectId, `/preview/.media/manifest.jsonl`))
.then((r) => {
if (!r.ok) {
manifest404Ref.current.add(projectId);
Expand Down
3 changes: 2 additions & 1 deletion packages/studio/src/components/sidebar/CompositionsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { buildProjectApiPath } from "../../utils/projectRouting";
import { memo, useCallback, useEffect, useRef, useState } from "react";
import { buildCompositionThumbnailUrl } from "../../player/components/CompositionThumbnail";
import { setPreviewMediaMuted } from "../../player/lib/timelineIframeHelpers";
Expand Down Expand Up @@ -173,7 +174,7 @@ function CompCard({
setLivePreviewLoaded(false);
};
const name = comp.replace(/^compositions\//, "").replace(/\.html$/, "");
const previewUrl = `/api/projects/${projectId}/preview/comp/${comp}`;
const previewUrl = buildProjectApiPath(projectId, `/preview/comp/${comp}`);
const thumbnailUrl = buildCompositionThumbnailUrl({
previewUrl,
seekTime: THUMBNAIL_SEEK_TIME_SECONDS,
Expand Down
3 changes: 2 additions & 1 deletion packages/studio/src/components/storyboard/FramePoster.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { buildProjectApiPath } from "../../utils/projectRouting";
import { useEffect, useState } from "react";
import { buildCompositionThumbnailUrl } from "../../player/components/CompositionThumbnail";

Expand Down Expand Up @@ -51,7 +52,7 @@ export function FramePoster({
);
}
let url = buildCompositionThumbnailUrl({
previewUrl: `/api/projects/${projectId}/preview/comp/${src}`,
previewUrl: buildProjectApiPath(projectId, `/preview/comp/${src}`),
seekTime: seconds,
duration: 0,
origin: window.location.origin,
Expand Down
3 changes: 2 additions & 1 deletion packages/studio/src/hooks/useAskAgentModal.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { buildProjectApiPath } from "../utils/projectRouting";
import { useState, useCallback, useRef, useEffect } from "react";
import { copyTextToClipboard } from "../utils/clipboard";
import { readTagSnippetByTarget } from "../utils/sourcePatcher";
Expand Down Expand Up @@ -53,7 +54,7 @@ export function useAskAgentModal({
const targetPath = selection.sourceFile || activeCompPath || "index.html";
try {
const response = await fetch(
`/api/projects/${pid}/files/${encodeURIComponent(targetPath)}`,
buildProjectApiPath(pid, `/files/${encodeURIComponent(targetPath)}`),
);
if (!response.ok) return;

Expand Down
3 changes: 2 additions & 1 deletion packages/studio/src/hooks/useCaptionDetection.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { buildProjectApiPath } from "../utils/projectRouting";
import { useEffect, useRef } from "react";
import { useCaptionStore } from "../captions/store";
import { acceptStudioRuntimeMessage } from "../player/lib/runtimeProtocol";
Expand Down Expand Up @@ -107,7 +108,7 @@ export function useCaptionDetection({

activating = true;
const srcPath = captionSrcPath;
fetch(`/api/projects/${projectId}/files/${encodeURIComponent(srcPath)}`)
fetch(buildProjectApiPath(projectId, `/files/${encodeURIComponent(srcPath)}`))
.then((r) => r.json())
.then((data: { content?: string }) => {
if (!data.content || !doc || !win || useCaptionStore.getState().isEditMode) return;
Expand Down
4 changes: 3 additions & 1 deletion packages/studio/src/hooks/useCompositionContentLoader.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { buildProjectApiPath } from "../utils/projectRouting";
import { useCallback } from "react";

/**
Expand All @@ -20,9 +21,10 @@ export function useCompositionContentLoader({
}) {
return useCallback(
(comp: string) => {
if (!projectId) return;
setActiveCompPath(comp.endsWith(".html") ? comp : null);
setEditingFile({ path: comp, content: null });
fetch(`/api/projects/${projectId}/files/${comp}`)
fetch(buildProjectApiPath(projectId, `/files/${encodeURIComponent(comp)}`))
.then(async (r) => {
if (!r.ok) throw new Error(`Failed to load ${comp} (${r.status})`);
return r.json();
Expand Down
9 changes: 5 additions & 4 deletions packages/studio/src/hooks/useDomEditCommits.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { buildProjectApiPath } from "../utils/projectRouting";
import { useCallback, useRef } from "react";
import { findUnsafeDomPatchValues } from "@hyperframes/core/studio-api/finite-mutation";
import { FONT_EXT } from "../utils/mediaTypes";
Expand Down Expand Up @@ -127,11 +128,11 @@ export function useDomEditCommits({
FONT_EXT.test(path) &&
fontFamilyFromAssetPath(path).toLowerCase() === family.toLowerCase(),
);
if (!asset) return null;
if (!asset || !projectId) return null;
return {
family: fontFamilyFromAssetPath(asset),
path: asset,
url: `/api/projects/${projectId}/preview/${asset}`,
url: buildProjectApiPath(projectId, `/preview/${asset}`),
};
},
[fileTree, projectId, importedFontAssetsRef],
Expand Down Expand Up @@ -161,7 +162,7 @@ export function useDomEditCommits({
};

const readResponse = await fetch(
`/api/projects/${pid}/files/${encodeURIComponent(targetPath)}`,
buildProjectApiPath(pid, `/files/${encodeURIComponent(targetPath)}`),
);
if (!readResponse.ok) {
throw await createStudioSaveHttpError(readResponse, `Failed to read ${targetPath}`);
Expand Down Expand Up @@ -219,7 +220,7 @@ export function useDomEditCommits({
domEditSaveTimestampRef.current = Date.now();

const patchResponse = await fetch(
`/api/projects/${pid}/file-mutations/patch-element/${encodeURIComponent(targetPath)}`,
buildProjectApiPath(pid, `/file-mutations/patch-element/${encodeURIComponent(targetPath)}`),
{
method: "POST",
headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
Expand Down
6 changes: 5 additions & 1 deletion packages/studio/src/hooks/useElementLifecycleOps.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { buildProjectApiPath } from "../utils/projectRouting";
import { useCallback } from "react";
import { usePlayerStore } from "../player";
import {
Expand Down Expand Up @@ -152,7 +153,10 @@ export function useElementLifecycleOps({
// selection runs to hundreds of members — the file ended up correct, but
// only after long enough that Delete looked like it had done nothing.
const removeResponse = await fetch(
`/api/projects/${pid}/file-mutations/remove-elements/${encodeURIComponent(targetPath)}`,
buildProjectApiPath(
pid,
`/file-mutations/remove-elements/${encodeURIComponent(targetPath)}`,
),
{
method: "POST",
headers: { "Content-Type": "application/json", ...studioWriteHeaders() },
Expand Down
Loading
Loading