@hyperframes/studio-server 0.7.60 → 0.7.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-4ETS2LXI.js +192 -0
- package/dist/chunk-4ETS2LXI.js.map +1 -0
- package/dist/chunk-6XMC64FJ.js +584 -0
- package/dist/chunk-6XMC64FJ.js.map +1 -0
- package/dist/chunk-7Q5AFHU6.js +279 -0
- package/dist/chunk-7Q5AFHU6.js.map +1 -0
- package/dist/chunk-IVOJZ24X.js +457 -0
- package/dist/chunk-IVOJZ24X.js.map +1 -0
- package/dist/chunk-OK7FKBKI.js +66 -0
- package/dist/chunk-OK7FKBKI.js.map +1 -0
- package/dist/chunk-VPA335OG.js +437 -0
- package/dist/chunk-VPA335OG.js.map +1 -0
- package/dist/chunk-W2SBTCO2.js +32 -0
- package/dist/chunk-W2SBTCO2.js.map +1 -0
- package/dist/chunk-X62ASOGO.js +30 -0
- package/dist/chunk-X62ASOGO.js.map +1 -0
- package/dist/helpers/finiteMutation.js +4 -24
- package/dist/helpers/finiteMutation.js.map +1 -1
- package/dist/helpers/manualEditsRenderScript.js +5 -577
- package/dist/helpers/manualEditsRenderScript.js.map +1 -1
- package/dist/helpers/mediaCodecMap.d.ts +100 -0
- package/dist/helpers/mediaCodecMap.js +25 -0
- package/dist/helpers/mediaCodecMap.js.map +1 -0
- package/dist/helpers/mediaProxyPreview.d.ts +4 -0
- package/dist/helpers/mediaProxyPreview.js +17 -0
- package/dist/helpers/mediaProxyPreview.js.map +1 -0
- package/dist/helpers/proxyTranscoder.d.ts +61 -0
- package/dist/helpers/proxyTranscoder.js +30 -0
- package/dist/helpers/proxyTranscoder.js.map +1 -0
- package/dist/helpers/screenshotClip.js +3 -22
- package/dist/helpers/screenshotClip.js.map +1 -1
- package/dist/helpers/sourceMutation.d.ts +3 -0
- package/dist/helpers/sourceMutation.js +9 -419
- package/dist/helpers/sourceMutation.js.map +1 -1
- package/dist/helpers/studioMotionRenderScript.js +4 -186
- package/dist/helpers/studioMotionRenderScript.js.map +1 -1
- package/dist/index.d.ts +6 -202
- package/dist/index.js +678 -1423
- package/dist/index.js.map +1 -1
- package/dist/mediaProxyPreview-ChE1ATG4.d.ts +259 -0
- package/package.json +15 -3
package/dist/index.js
CHANGED
|
@@ -1,3 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findUnsafeDomPatchValues,
|
|
3
|
+
findUnsafeMutationValues
|
|
4
|
+
} from "./chunk-X62ASOGO.js";
|
|
5
|
+
import {
|
|
6
|
+
isHTMLElement,
|
|
7
|
+
patchElementInHtml,
|
|
8
|
+
probeElementInSource,
|
|
9
|
+
removeElementFromHtml,
|
|
10
|
+
splitElementInHtml,
|
|
11
|
+
unwrapElementsFromHtml,
|
|
12
|
+
wrapElementsInHtml
|
|
13
|
+
} from "./chunk-VPA335OG.js";
|
|
14
|
+
import {
|
|
15
|
+
getElementScreenshotClip
|
|
16
|
+
} from "./chunk-W2SBTCO2.js";
|
|
17
|
+
import {
|
|
18
|
+
injectMediaCodecMap,
|
|
19
|
+
isAutoProxyEnabled,
|
|
20
|
+
proxyEtagSalt,
|
|
21
|
+
resolvePreviewMediaCodecProbeCache
|
|
22
|
+
} from "./chunk-OK7FKBKI.js";
|
|
23
|
+
import {
|
|
24
|
+
ProxyCapacityError,
|
|
25
|
+
ProxyTranscodeError,
|
|
26
|
+
resolveProxy
|
|
27
|
+
} from "./chunk-IVOJZ24X.js";
|
|
28
|
+
import {
|
|
29
|
+
PROXY_VARIANT_CONFIG,
|
|
30
|
+
decideMediaProxyEligibility,
|
|
31
|
+
isProxyVariantRequest,
|
|
32
|
+
probeAssetCodec,
|
|
33
|
+
probeMediaMetadata,
|
|
34
|
+
resolveProxyVariantRequest
|
|
35
|
+
} from "./chunk-7Q5AFHU6.js";
|
|
36
|
+
import {
|
|
37
|
+
STUDIO_MANUAL_EDITS_PATH,
|
|
38
|
+
createStudioManualEditsRenderBodyScript,
|
|
39
|
+
createStudioPositionSeekReapplyScript
|
|
40
|
+
} from "./chunk-6XMC64FJ.js";
|
|
41
|
+
import {
|
|
42
|
+
STUDIO_MOTION_PATH,
|
|
43
|
+
createStudioMotionRenderBodyScript
|
|
44
|
+
} from "./chunk-4ETS2LXI.js";
|
|
45
|
+
|
|
1
46
|
// src/createStudioApi.ts
|
|
2
47
|
import { Hono } from "hono";
|
|
3
48
|
|
|
@@ -289,10 +334,10 @@ function registerStoryboardRoutes(api, adapter) {
|
|
|
289
334
|
import { bodyLimit } from "hono/body-limit";
|
|
290
335
|
import {
|
|
291
336
|
closeSync,
|
|
292
|
-
existsSync as
|
|
337
|
+
existsSync as existsSync4,
|
|
293
338
|
ftruncateSync,
|
|
294
339
|
openSync,
|
|
295
|
-
readFileSync as
|
|
340
|
+
readFileSync as readFileSync5,
|
|
296
341
|
writeFileSync as writeFileSync4,
|
|
297
342
|
writeSync,
|
|
298
343
|
mkdirSync as mkdirSync3,
|
|
@@ -302,7 +347,7 @@ import {
|
|
|
302
347
|
renameSync,
|
|
303
348
|
readdirSync as readdirSync4
|
|
304
349
|
} from "fs";
|
|
305
|
-
import { resolve as resolve3, dirname, join as join6 } from "path";
|
|
350
|
+
import { resolve as resolve3, dirname as dirname2, join as join6 } from "path";
|
|
306
351
|
|
|
307
352
|
// src/helpers/mime.ts
|
|
308
353
|
var MIME_TYPES = {
|
|
@@ -319,7 +364,13 @@ var MIME_TYPES = {
|
|
|
319
364
|
".webp": "image/webp",
|
|
320
365
|
".ico": "image/x-icon",
|
|
321
366
|
".mp4": "video/mp4",
|
|
367
|
+
".m4v": "video/mp4",
|
|
322
368
|
".mov": "video/quicktime",
|
|
369
|
+
".mkv": "video/x-matroska",
|
|
370
|
+
".mxf": "video/mxf",
|
|
371
|
+
".mts": "video/mp2t",
|
|
372
|
+
".m2ts": "video/mp2t",
|
|
373
|
+
".ts": "video/mp2t",
|
|
323
374
|
".webm": "video/webm",
|
|
324
375
|
".mp3": "audio/mpeg",
|
|
325
376
|
".wav": "audio/wav",
|
|
@@ -347,7 +398,8 @@ function isAudioFile(name) {
|
|
|
347
398
|
// src/helpers/waveform.ts
|
|
348
399
|
import { spawn } from "child_process";
|
|
349
400
|
import { existsSync as existsSync2, writeFileSync, mkdirSync } from "fs";
|
|
350
|
-
import { join as join3
|
|
401
|
+
import { join as join3 } from "path";
|
|
402
|
+
import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
|
|
351
403
|
var SAMPLE_RATE = 4e3;
|
|
352
404
|
var PEAK_COUNT = 4e3;
|
|
353
405
|
var WAVEFORM_CACHE_VERSION = "v2";
|
|
@@ -370,15 +422,10 @@ function computePeaks(floats, count) {
|
|
|
370
422
|
const maxPeak = Math.max(...peaks, 1e-3);
|
|
371
423
|
return peaks.map((p) => p / maxPeak);
|
|
372
424
|
}
|
|
373
|
-
function ffmpegBinary() {
|
|
374
|
-
const configured = process.env.HYPERFRAMES_FFMPEG_PATH?.trim();
|
|
375
|
-
if (configured) return resolve2(configured);
|
|
376
|
-
return "ffmpeg";
|
|
377
|
-
}
|
|
378
425
|
function decodeAudioPeaks(audioPath) {
|
|
379
426
|
return new Promise((resolvePromise, reject) => {
|
|
380
427
|
const proc = spawn(
|
|
381
|
-
|
|
428
|
+
findFfBinary("ffmpeg") ?? "ffmpeg",
|
|
382
429
|
[
|
|
383
430
|
"-i",
|
|
384
431
|
audioPath,
|
|
@@ -430,7 +477,7 @@ import { spawnSync } from "child_process";
|
|
|
430
477
|
import { mkdtempSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
431
478
|
import { tmpdir } from "os";
|
|
432
479
|
import { basename, join as join4 } from "path";
|
|
433
|
-
var VIDEO_EXT = /\.(mp4|webm|mov)$/i;
|
|
480
|
+
var VIDEO_EXT = /\.(mp4|webm|mov|mkv|avi|m4v|mxf|mts|m2ts|ts)$/i;
|
|
434
481
|
var AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac)$/i;
|
|
435
482
|
function validateUploadedMedia(filePath, runner = spawnSync) {
|
|
436
483
|
const isVideo = VIDEO_EXT.test(filePath);
|
|
@@ -580,31 +627,6 @@ function consumeFileWriteReceipt(absPath) {
|
|
|
580
627
|
return { path, version, writeToken };
|
|
581
628
|
}
|
|
582
629
|
|
|
583
|
-
// src/helpers/finiteMutation.ts
|
|
584
|
-
function findUnsafeMutationValues(value, path = "body", options = {}) {
|
|
585
|
-
if (value === null) {
|
|
586
|
-
return options.allowNullPath?.(path) ? [] : [{ path, reason: "null" }];
|
|
587
|
-
}
|
|
588
|
-
if (typeof value === "number") {
|
|
589
|
-
return Number.isFinite(value) ? [] : [{ path, reason: "non-finite-number" }];
|
|
590
|
-
}
|
|
591
|
-
if (!value || typeof value !== "object") return [];
|
|
592
|
-
if (Array.isArray(value)) {
|
|
593
|
-
return value.flatMap(
|
|
594
|
-
(item, index) => findUnsafeMutationValues(item, `${path}[${index}]`, options)
|
|
595
|
-
);
|
|
596
|
-
}
|
|
597
|
-
return Object.entries(value).flatMap(
|
|
598
|
-
([key, item]) => findUnsafeMutationValues(item, `${path}.${key}`, options)
|
|
599
|
-
);
|
|
600
|
-
}
|
|
601
|
-
var DOM_PATCH_NULL_VALUE_PATH = /^body\.operations\[\d+\]\.value$/;
|
|
602
|
-
function findUnsafeDomPatchValues(value) {
|
|
603
|
-
return findUnsafeMutationValues(value, "body", {
|
|
604
|
-
allowNullPath: (path) => DOM_PATCH_NULL_VALUE_PATH.test(path)
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
|
|
608
630
|
// src/routes/files.ts
|
|
609
631
|
import { classifyPropertyGroup } from "@hyperframes/parsers/gsap-constants";
|
|
610
632
|
import { parseGsapScriptAcorn } from "@hyperframes/parsers/gsap-parser-acorn";
|
|
@@ -632,429 +654,196 @@ import {
|
|
|
632
654
|
scalePositionsInScript,
|
|
633
655
|
dedupePositionWritesInScript
|
|
634
656
|
} from "@hyperframes/parsers/gsap-writer-acorn";
|
|
657
|
+
import { parseHTML as parseHTML2 } from "linkedom";
|
|
635
658
|
|
|
636
|
-
// src/helpers/
|
|
659
|
+
// src/helpers/compositionInsertion.ts
|
|
660
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, realpathSync } from "fs";
|
|
661
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
662
|
+
import { dirname, relative as relative3, resolve as resolve2, sep } from "path";
|
|
637
663
|
import { parseHTML } from "linkedom";
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
// src/helpers/sourceStyleMutation.ts
|
|
644
|
-
function parseStyleDecls(style) {
|
|
645
|
-
const props = /* @__PURE__ */ new Map();
|
|
646
|
-
const order = [];
|
|
647
|
-
let i = 0;
|
|
648
|
-
while (i < style.length) {
|
|
649
|
-
let depth = 0;
|
|
650
|
-
let inSingle = false;
|
|
651
|
-
let inDouble = false;
|
|
652
|
-
const start = i;
|
|
653
|
-
while (i < style.length) {
|
|
654
|
-
const ch = style[i];
|
|
655
|
-
if (ch === "'" && !inDouble) inSingle = !inSingle;
|
|
656
|
-
else if (ch === '"' && !inSingle) inDouble = !inDouble;
|
|
657
|
-
else if (!inSingle && !inDouble) {
|
|
658
|
-
if (ch === "(") depth++;
|
|
659
|
-
else if (ch === ")") depth = Math.max(0, depth - 1);
|
|
660
|
-
else if (ch === ";" && depth === 0) break;
|
|
661
|
-
}
|
|
662
|
-
i++;
|
|
663
|
-
}
|
|
664
|
-
const decl = style.slice(start, i).trim();
|
|
665
|
-
i++;
|
|
666
|
-
if (!decl) continue;
|
|
667
|
-
const colon = decl.indexOf(":");
|
|
668
|
-
if (colon < 0) continue;
|
|
669
|
-
const key = decl.slice(0, colon).trim();
|
|
670
|
-
const val = decl.slice(colon + 1).trim();
|
|
671
|
-
if (!key) continue;
|
|
672
|
-
if (!props.has(key)) order.push(key);
|
|
673
|
-
props.set(key, val);
|
|
674
|
-
}
|
|
675
|
-
return { props, order };
|
|
676
|
-
}
|
|
677
|
-
function serializeStyleDecls(props, order) {
|
|
678
|
-
return order.map((k) => `${k}: ${props.get(k) ?? ""}`).filter((d) => d.trim()).join("; ");
|
|
679
|
-
}
|
|
680
|
-
function patchStyleAttrString(style, property, value) {
|
|
681
|
-
const { props, order } = parseStyleDecls(style);
|
|
682
|
-
if (value === null) {
|
|
683
|
-
props.delete(property);
|
|
684
|
-
const idx = order.indexOf(property);
|
|
685
|
-
if (idx >= 0) order.splice(idx, 1);
|
|
686
|
-
} else {
|
|
687
|
-
if (!props.has(property)) order.push(property);
|
|
688
|
-
props.set(property, value);
|
|
664
|
+
var CompositionInsertionError = class extends Error {
|
|
665
|
+
constructor(message, status) {
|
|
666
|
+
super(message);
|
|
667
|
+
this.status = status;
|
|
689
668
|
}
|
|
690
|
-
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
if (hasDocumentShell) {
|
|
697
|
-
return { document: parseHTML(source).document, wrappedFragment: false };
|
|
698
|
-
}
|
|
699
|
-
return {
|
|
700
|
-
document: parseHTML(`<!DOCTYPE html><html><head></head><body>${source}</body></html>`).document,
|
|
701
|
-
wrappedFragment: true
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
function duplicateCssRulesForId(document2, originalId, newId) {
|
|
705
|
-
const idToken = `#${originalId}`;
|
|
706
|
-
const transform = selectorParser((selectors) => {
|
|
707
|
-
selectors.walkIds((node) => {
|
|
708
|
-
if (node.value === originalId) node.value = newId;
|
|
709
|
-
});
|
|
710
|
-
});
|
|
711
|
-
for (const styleEl of document2.querySelectorAll("style")) {
|
|
712
|
-
const css = styleEl.textContent ?? "";
|
|
713
|
-
let root;
|
|
714
|
-
try {
|
|
715
|
-
root = postcss.parse(css);
|
|
716
|
-
} catch {
|
|
717
|
-
continue;
|
|
718
|
-
}
|
|
719
|
-
const clones = [];
|
|
720
|
-
root.walkRules((rule) => {
|
|
721
|
-
if (!rule.selector.includes(idToken)) return;
|
|
722
|
-
const newSelector = transform.processSync(rule.selector);
|
|
723
|
-
if (newSelector === rule.selector) return;
|
|
724
|
-
const clone = rule.clone({ selector: newSelector });
|
|
725
|
-
clones.push(clone);
|
|
726
|
-
});
|
|
727
|
-
if (clones.length > 0) {
|
|
728
|
-
for (const c of clones) root.append(c);
|
|
729
|
-
styleEl.textContent = root.toString();
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
function querySelectorAllWithTemplates(root, selector) {
|
|
734
|
-
const matches = Array.from(root.querySelectorAll(selector));
|
|
735
|
-
if (matches.length > 0) return matches;
|
|
736
|
-
const templates = Array.from(root.querySelectorAll("template"));
|
|
737
|
-
for (const tmpl of templates) {
|
|
738
|
-
const inner = querySelectorAllWithTemplates(tmpl, selector);
|
|
739
|
-
if (inner.length > 0) return inner;
|
|
669
|
+
status;
|
|
670
|
+
};
|
|
671
|
+
function descendants(root, selector) {
|
|
672
|
+
const found = Array.from(root.querySelectorAll(selector));
|
|
673
|
+
for (const template of root.querySelectorAll("template")) {
|
|
674
|
+
found.push(...descendants(template, selector));
|
|
740
675
|
}
|
|
741
|
-
return [];
|
|
676
|
+
return [...new Set(found)];
|
|
742
677
|
}
|
|
743
|
-
function
|
|
744
|
-
|
|
678
|
+
function compositionRoot(source) {
|
|
679
|
+
const document = parseHTML(source).document;
|
|
680
|
+
const root = descendants(document, "[data-composition-id]")[0];
|
|
681
|
+
if (!root) throw new CompositionInsertionError("Composition source has no root", 400);
|
|
682
|
+
return { document, root };
|
|
745
683
|
}
|
|
746
|
-
function
|
|
747
|
-
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
`[data-hf-id="${escapeCssAttrValue(hfId)}"]`
|
|
751
|
-
);
|
|
752
|
-
if (matches.length > 1) {
|
|
753
|
-
console.warn(
|
|
754
|
-
`sourceMutation: data-hf-id "${hfId}" matched ${matches.length} elements; using the first. ids must be unique per document.`
|
|
755
|
-
);
|
|
756
|
-
}
|
|
757
|
-
return matches[0] ?? null;
|
|
758
|
-
} catch {
|
|
759
|
-
return null;
|
|
684
|
+
function positiveAttribute(root, ...names) {
|
|
685
|
+
for (const name of names) {
|
|
686
|
+
const value = Number.parseFloat(root.getAttribute(name) ?? "");
|
|
687
|
+
if (Number.isFinite(value) && value > 0) return value;
|
|
760
688
|
}
|
|
689
|
+
throw new CompositionInsertionError(`Composition source has no valid ${names[0]}`, 400);
|
|
761
690
|
}
|
|
762
|
-
function
|
|
763
|
-
if (
|
|
764
|
-
|
|
765
|
-
if (el) return el;
|
|
691
|
+
function canonicalProjectPath(projectDir, candidate) {
|
|
692
|
+
if (!candidate) {
|
|
693
|
+
throw new CompositionInsertionError("Composition source escapes the project", 400);
|
|
766
694
|
}
|
|
767
|
-
if (
|
|
768
|
-
|
|
769
|
-
if (byId) return byId;
|
|
695
|
+
if (!existsSync3(candidate)) {
|
|
696
|
+
throw new CompositionInsertionError("Composition source was not found", 404);
|
|
770
697
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
return matches[target.selectorIndex ?? 0] ?? null;
|
|
775
|
-
} catch {
|
|
776
|
-
return null;
|
|
698
|
+
const canonical = realpathSync(candidate);
|
|
699
|
+
if (!isSafePath(realpathSync(projectDir), canonical)) {
|
|
700
|
+
throw new CompositionInsertionError("Composition source escapes the project", 400);
|
|
777
701
|
}
|
|
702
|
+
return canonical;
|
|
778
703
|
}
|
|
779
|
-
function
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
if (!element) return source;
|
|
783
|
-
element.remove();
|
|
784
|
-
return wrappedFragment ? document2.body.innerHTML || "" : document2.toString();
|
|
785
|
-
}
|
|
786
|
-
function isHTMLElement(el) {
|
|
787
|
-
const HTMLEl = el.ownerDocument?.defaultView?.HTMLElement;
|
|
788
|
-
return HTMLEl ? el instanceof HTMLEl : el.nodeType === 1 && "style" in el;
|
|
789
|
-
}
|
|
790
|
-
function resolveOperationTarget(parent, op) {
|
|
791
|
-
if (op.childSelector === void 0) return parent;
|
|
792
|
-
try {
|
|
793
|
-
const child = parent.querySelectorAll(op.childSelector)[op.childIndex ?? 0] ?? null;
|
|
794
|
-
return child && isHTMLElement(child) ? child : null;
|
|
795
|
-
} catch {
|
|
796
|
-
return null;
|
|
704
|
+
function validateSourcePath(sourcePath) {
|
|
705
|
+
if (!sourcePath.trim() || sourcePath.includes("\0") || /^[a-z]+:/i.test(sourcePath)) {
|
|
706
|
+
throw new CompositionInsertionError("Invalid composition source path", 400);
|
|
797
707
|
}
|
|
798
708
|
}
|
|
799
|
-
function
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
if (!el || !isHTMLElement(el)) return { html: source, matched: false };
|
|
803
|
-
const htmlEl = el;
|
|
804
|
-
const resolved = [];
|
|
805
|
-
for (const op of operations) {
|
|
806
|
-
const opTarget = resolveOperationTarget(htmlEl, op);
|
|
807
|
-
if (!opTarget) return { html: source, matched: false };
|
|
808
|
-
resolved.push({ op, target: opTarget });
|
|
809
|
-
}
|
|
810
|
-
for (const { op, target: opTarget } of resolved) {
|
|
811
|
-
switch (op.type) {
|
|
812
|
-
case "inline-style":
|
|
813
|
-
{
|
|
814
|
-
const raw = opTarget.getAttribute("style") ?? "";
|
|
815
|
-
const patched = patchStyleAttrString(raw, op.property, op.value);
|
|
816
|
-
opTarget.setAttribute("style", patched);
|
|
817
|
-
}
|
|
818
|
-
break;
|
|
819
|
-
case "attribute":
|
|
820
|
-
{
|
|
821
|
-
const fullAttr = op.property.startsWith("data-") ? op.property : `data-${op.property}`;
|
|
822
|
-
if (op.value != null) {
|
|
823
|
-
opTarget.setAttribute(fullAttr, op.value);
|
|
824
|
-
} else {
|
|
825
|
-
opTarget.removeAttribute(fullAttr);
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
break;
|
|
829
|
-
case "html-attribute":
|
|
830
|
-
if (!isAllowedHtmlAttribute(op.property)) break;
|
|
831
|
-
if (op.value != null) {
|
|
832
|
-
if (!isSafeAttributeValue(op.property, op.value)) break;
|
|
833
|
-
opTarget.setAttribute(op.property, op.value);
|
|
834
|
-
} else {
|
|
835
|
-
opTarget.removeAttribute(op.property);
|
|
836
|
-
}
|
|
837
|
-
break;
|
|
838
|
-
case "text-content":
|
|
839
|
-
if (op.value != null) {
|
|
840
|
-
const inner = opTarget.children.length === 1 ? opTarget.firstElementChild : null;
|
|
841
|
-
const textTarget = inner && isHTMLElement(inner) ? inner : opTarget;
|
|
842
|
-
textTarget.textContent = op.value;
|
|
843
|
-
}
|
|
844
|
-
break;
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
return {
|
|
848
|
-
html: wrappedFragment ? document2.body.innerHTML || "" : document2.toString(),
|
|
849
|
-
matched: true
|
|
850
|
-
};
|
|
709
|
+
function canonicalProjectFile(projectDir, sourcePath) {
|
|
710
|
+
validateSourcePath(sourcePath);
|
|
711
|
+
return canonicalProjectPath(projectDir, resolveWithinProject(projectDir, sourcePath));
|
|
851
712
|
}
|
|
852
|
-
function
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
function resolveElementTiming(el) {
|
|
859
|
-
const start = parseFloat(el.getAttribute("data-start") ?? "0") || 0;
|
|
860
|
-
const usesDataEnd = el.hasAttribute("data-end");
|
|
861
|
-
const duration = usesDataEnd ? parseFloat(el.getAttribute("data-end") ?? "") - start || 0 : parseFloat(el.getAttribute("data-duration") ?? "0") || 0;
|
|
862
|
-
return { start, duration, usesDataEnd };
|
|
863
|
-
}
|
|
864
|
-
function setElementDuration(el, start, duration, usesDataEnd) {
|
|
865
|
-
if (usesDataEnd) {
|
|
866
|
-
const endTime = String(Math.round((start + duration) * 1e3) / 1e3);
|
|
867
|
-
el.setAttribute("data-end", endTime);
|
|
868
|
-
el.removeAttribute("data-duration");
|
|
869
|
-
} else {
|
|
870
|
-
el.setAttribute("data-duration", String(Math.round(duration * 1e3) / 1e3));
|
|
871
|
-
el.removeAttribute("data-end");
|
|
872
|
-
}
|
|
713
|
+
function canonicalDependency(projectDir, ownerAbs, sourcePath) {
|
|
714
|
+
validateSourcePath(sourcePath);
|
|
715
|
+
return canonicalProjectPath(
|
|
716
|
+
projectDir,
|
|
717
|
+
resolveWithinProject(projectDir, relative3(projectDir, resolve2(dirname(ownerAbs), sourcePath)))
|
|
718
|
+
);
|
|
873
719
|
}
|
|
874
|
-
function
|
|
875
|
-
const
|
|
876
|
-
const
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
720
|
+
function validateDependencyGraph(projectDir, targetAbs, sourceAbs) {
|
|
721
|
+
const visited = /* @__PURE__ */ new Set();
|
|
722
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
723
|
+
const visit = (file) => {
|
|
724
|
+
if (file === targetAbs) {
|
|
725
|
+
throw new CompositionInsertionError("Composition insertion would create a cycle", 400);
|
|
726
|
+
}
|
|
727
|
+
if (visiting.has(file)) {
|
|
728
|
+
throw new CompositionInsertionError("Composition dependency cycle detected", 400);
|
|
729
|
+
}
|
|
730
|
+
if (visited.has(file)) return;
|
|
731
|
+
visiting.add(file);
|
|
732
|
+
const source = readFileSync4(file, "utf-8");
|
|
733
|
+
const { document } = compositionRoot(source);
|
|
734
|
+
for (const host of descendants(document, "[data-composition-src]")) {
|
|
735
|
+
const dependency = host.getAttribute("data-composition-src");
|
|
736
|
+
if (dependency) {
|
|
737
|
+
visit(canonicalDependency(projectDir, file, dependency));
|
|
738
|
+
}
|
|
893
739
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
const secondDuration = duration - firstDuration;
|
|
897
|
-
const clone = el.cloneNode(true);
|
|
898
|
-
if (!isHTMLElement(clone)) return { html: source, matched: false, newId: null };
|
|
899
|
-
clone.setAttribute("id", newId);
|
|
900
|
-
clone.removeAttribute("data-hf-id");
|
|
901
|
-
for (const node of clone.querySelectorAll("[data-hf-id]")) node.removeAttribute("data-hf-id");
|
|
902
|
-
clone.setAttribute("data-start", String(Math.round(splitTime * 1e3) / 1e3));
|
|
903
|
-
setElementDuration(clone, splitTime, secondDuration, usesDataEnd);
|
|
904
|
-
const playbackStartAttr = el.hasAttribute("data-playback-start") ? "data-playback-start" : el.hasAttribute("data-media-start") ? "data-media-start" : null;
|
|
905
|
-
if (playbackStartAttr) {
|
|
906
|
-
const currentTrim = parseFloat(el.getAttribute(playbackStartAttr) ?? "0") || 0;
|
|
907
|
-
const rateRaw = parseFloat(el.getAttribute("data-playback-rate") ?? "");
|
|
908
|
-
const rate = Number.isFinite(rateRaw) ? rateRaw : 1;
|
|
909
|
-
clone.setAttribute(
|
|
910
|
-
playbackStartAttr,
|
|
911
|
-
String(Math.round((currentTrim + firstDuration * rate) * 1e3) / 1e3)
|
|
912
|
-
);
|
|
913
|
-
}
|
|
914
|
-
const originalId = el.getAttribute("id");
|
|
915
|
-
if (originalId) {
|
|
916
|
-
duplicateCssRulesForId(document2, originalId, newId);
|
|
917
|
-
}
|
|
918
|
-
el.setAttribute("data-start", String(Math.round(start * 1e3) / 1e3));
|
|
919
|
-
setElementDuration(el, start, firstDuration, usesDataEnd);
|
|
920
|
-
if (el.nextSibling) {
|
|
921
|
-
el.parentElement.insertBefore(clone, el.nextSibling);
|
|
922
|
-
} else {
|
|
923
|
-
el.parentElement.appendChild(clone);
|
|
924
|
-
}
|
|
925
|
-
const html = wrappedFragment ? document2.body.innerHTML || "" : document2.toString();
|
|
926
|
-
return {
|
|
927
|
-
// The split owns its new nodes' stable ids. Leaving the clone unstamped makes
|
|
928
|
-
// the next preview request persist different bytes after history is recorded.
|
|
929
|
-
html: ensureHfIds(html),
|
|
930
|
-
matched: true,
|
|
931
|
-
newId
|
|
740
|
+
visiting.delete(file);
|
|
741
|
+
visited.add(file);
|
|
932
742
|
};
|
|
743
|
+
visit(sourceAbs);
|
|
933
744
|
}
|
|
934
|
-
function
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
const raw = props.get(property);
|
|
938
|
-
if (!raw) return 0;
|
|
939
|
-
const n = parseFloat(raw);
|
|
940
|
-
return Number.isFinite(n) ? n : 0;
|
|
941
|
-
}
|
|
942
|
-
function setInlineLeftTop(el, left, top) {
|
|
943
|
-
let style = el.getAttribute("style") ?? "";
|
|
944
|
-
style = patchStyleAttrString(style, "left", `${left}px`);
|
|
945
|
-
style = patchStyleAttrString(style, "top", `${top}px`);
|
|
946
|
-
el.setAttribute("style", style);
|
|
947
|
-
}
|
|
948
|
-
function uniqueGroupDomId(document2, groupId) {
|
|
949
|
-
const base = groupId.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "group";
|
|
950
|
-
let id = base;
|
|
951
|
-
let n = 2;
|
|
952
|
-
while (document2.getElementById(id)) {
|
|
953
|
-
id = `${base}-${n}`;
|
|
954
|
-
n += 1;
|
|
955
|
-
}
|
|
956
|
-
return id;
|
|
745
|
+
function numberAttribute(element, name, fallback = 0) {
|
|
746
|
+
const value = Number.parseFloat(element.getAttribute(name) ?? "");
|
|
747
|
+
return Number.isFinite(value) ? value : fallback;
|
|
957
748
|
}
|
|
958
|
-
function
|
|
959
|
-
const
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
const
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
749
|
+
function rangesOverlap(start, duration, other) {
|
|
750
|
+
const otherStart = numberAttribute(other, "data-start");
|
|
751
|
+
const otherDuration = numberAttribute(other, "data-duration");
|
|
752
|
+
return start < otherStart + otherDuration && otherStart < start + duration;
|
|
753
|
+
}
|
|
754
|
+
function resolveTrack(root, desiredTrack, start, duration) {
|
|
755
|
+
const clips = descendants(root, "[data-start][data-duration]").filter(
|
|
756
|
+
(element) => element !== root && element.parentElement?.closest("[data-composition-id]") === root
|
|
757
|
+
);
|
|
758
|
+
const tracks = [...new Set(clips.map((clip) => numberAttribute(clip, "data-track-index")))].sort(
|
|
759
|
+
(a, b) => a - b
|
|
760
|
+
);
|
|
761
|
+
const isFree = (track) => !clips.some(
|
|
762
|
+
(clip) => numberAttribute(clip, "data-track-index") === track && rangesOverlap(start, duration, clip)
|
|
763
|
+
);
|
|
764
|
+
if (isFree(desiredTrack)) return desiredTrack;
|
|
765
|
+
const row = tracks.indexOf(desiredTrack);
|
|
766
|
+
for (let index = row - 1; index >= 0; index--) {
|
|
767
|
+
const track = tracks[index];
|
|
768
|
+
if (track !== void 0 && isFree(track)) return track;
|
|
769
|
+
}
|
|
770
|
+
for (let index = Math.max(0, row + 1); index < tracks.length; index++) {
|
|
771
|
+
const track = tracks[index];
|
|
772
|
+
if (track !== void 0 && isFree(track)) return track;
|
|
773
|
+
}
|
|
774
|
+
return Math.max(desiredTrack, ...tracks, -1) + 1;
|
|
775
|
+
}
|
|
776
|
+
function uniqueHostId(root, base) {
|
|
777
|
+
const ids = /* @__PURE__ */ new Set([
|
|
778
|
+
...descendants(root, "[id]").map((element) => element.id),
|
|
779
|
+
...descendants(root, "[data-composition-id]").flatMap((element) => {
|
|
780
|
+
const id = element.getAttribute("data-composition-id");
|
|
781
|
+
return id ? [id] : [];
|
|
782
|
+
})
|
|
783
|
+
]);
|
|
784
|
+
if (!ids.has(base)) return base;
|
|
785
|
+
let suffix = 2;
|
|
786
|
+
while (ids.has(`${base}_${suffix}`)) suffix += 1;
|
|
787
|
+
return `${base}_${suffix}`;
|
|
788
|
+
}
|
|
789
|
+
function relativeSourcePath(targetAbs, sourceAbs) {
|
|
790
|
+
return relative3(dirname(targetAbs), sourceAbs).split(sep).join("/");
|
|
791
|
+
}
|
|
792
|
+
function insertCompositionIntoSource(input) {
|
|
793
|
+
const targetAbs = canonicalProjectFile(input.projectDir, input.targetPath);
|
|
794
|
+
const sourceAbs = canonicalProjectFile(input.projectDir, input.sourcePath);
|
|
795
|
+
validateDependencyGraph(input.projectDir, targetAbs, sourceAbs);
|
|
796
|
+
const source = readFileSync4(sourceAbs, "utf-8");
|
|
797
|
+
const sourceComposition = compositionRoot(source).root;
|
|
798
|
+
const duration = positiveAttribute(
|
|
799
|
+
sourceComposition,
|
|
800
|
+
"data-composition-duration",
|
|
801
|
+
"data-duration"
|
|
802
|
+
);
|
|
803
|
+
const width = positiveAttribute(sourceComposition, "data-width");
|
|
804
|
+
const height = positiveAttribute(sourceComposition, "data-height");
|
|
805
|
+
const { document, root } = compositionRoot(input.parentSource);
|
|
806
|
+
const parentDuration = positiveAttribute(root, "data-duration", "data-composition-duration");
|
|
807
|
+
const base = (sourceComposition.getAttribute("data-composition-id") ?? "composition").replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "composition";
|
|
808
|
+
const hostId = uniqueHostId(root, base);
|
|
809
|
+
const track = resolveTrack(
|
|
810
|
+
root,
|
|
811
|
+
Math.max(0, Math.round(input.desiredTrack)),
|
|
812
|
+
input.start,
|
|
813
|
+
duration
|
|
814
|
+
);
|
|
815
|
+
const zIndex = Math.max(
|
|
816
|
+
0,
|
|
817
|
+
...descendants(root, "[style]").map((element) => {
|
|
818
|
+
const match = /(?:^|;)\s*z-index\s*:\s*(-?\d+)/i.exec(element.getAttribute("style") ?? "");
|
|
819
|
+
return match?.[1] ? Number.parseInt(match[1], 10) : 0;
|
|
820
|
+
})
|
|
821
|
+
) + 1;
|
|
822
|
+
const host = document.createElement("div");
|
|
823
|
+
host.id = hostId;
|
|
824
|
+
host.className = "clip";
|
|
825
|
+
host.setAttribute("data-hf-id", `hf-${randomUUID2()}`);
|
|
826
|
+
host.setAttribute("data-composition-id", hostId);
|
|
827
|
+
host.setAttribute("data-composition-src", relativeSourcePath(targetAbs, sourceAbs));
|
|
828
|
+
host.setAttribute("data-start", String(Math.round(input.start * 100) / 100));
|
|
829
|
+
host.setAttribute("data-duration", String(duration));
|
|
830
|
+
host.setAttribute("data-playback-start", "0");
|
|
831
|
+
host.setAttribute("data-track-index", String(track));
|
|
832
|
+
host.setAttribute("data-width", String(width));
|
|
833
|
+
host.setAttribute("data-height", String(height));
|
|
834
|
+
host.setAttribute(
|
|
1001
835
|
"style",
|
|
1002
|
-
`position: absolute; left:
|
|
836
|
+
`position: absolute; left: 0px; top: 0px; width: ${width}px; height: ${height}px; z-index: ${zIndex}`
|
|
1003
837
|
);
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
const
|
|
1007
|
-
|
|
1008
|
-
wrapper.appendChild(el);
|
|
1009
|
-
}
|
|
1010
|
-
return {
|
|
1011
|
-
html: wrappedFragment ? document2.body.innerHTML || "" : document2.toString(),
|
|
1012
|
-
matched: true,
|
|
1013
|
-
groupId
|
|
1014
|
-
};
|
|
1015
|
-
}
|
|
1016
|
-
function unwrapElementsFromHtml(source, groupTarget) {
|
|
1017
|
-
const { document: document2, wrappedFragment } = parseSourceDocument(source);
|
|
1018
|
-
const group = findTargetElement(document2, groupTarget);
|
|
1019
|
-
if (!group || !isHTMLElement(group)) return { html: source, unwrapped: false };
|
|
1020
|
-
if (!group.hasAttribute("data-hf-group")) return { html: source, unwrapped: false };
|
|
1021
|
-
const parent = group.parentElement;
|
|
1022
|
-
if (!parent) return { html: source, unwrapped: false };
|
|
1023
|
-
const wLeft = getInlineStylePx(group, "left");
|
|
1024
|
-
const wTop = getInlineStylePx(group, "top");
|
|
1025
|
-
const groupCenter = {
|
|
1026
|
-
cx: wLeft + getInlineStylePx(group, "width") / 2,
|
|
1027
|
-
cy: wTop + getInlineStylePx(group, "height") / 2
|
|
1028
|
-
};
|
|
1029
|
-
const members = [];
|
|
1030
|
-
for (const child of Array.from(group.children)) {
|
|
1031
|
-
if (isHTMLElement(child)) {
|
|
1032
|
-
const newLeft = getInlineStylePx(child, "left") + wLeft;
|
|
1033
|
-
const newTop = getInlineStylePx(child, "top") + wTop;
|
|
1034
|
-
setInlineLeftTop(child, newLeft, newTop);
|
|
1035
|
-
if (child.id) {
|
|
1036
|
-
members.push({
|
|
1037
|
-
id: child.id,
|
|
1038
|
-
cx: newLeft + getInlineStylePx(child, "width") / 2,
|
|
1039
|
-
cy: newTop + getInlineStylePx(child, "height") / 2
|
|
1040
|
-
});
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
parent.insertBefore(child, group);
|
|
838
|
+
root.appendChild(host);
|
|
839
|
+
if (input.start + duration > parentDuration) {
|
|
840
|
+
const name = root.hasAttribute("data-duration") ? "data-duration" : "data-composition-duration";
|
|
841
|
+
root.setAttribute(name, String(Math.round((input.start + duration) * 100) / 100));
|
|
1044
842
|
}
|
|
1045
|
-
|
|
1046
|
-
group.remove();
|
|
1047
|
-
return {
|
|
1048
|
-
html: wrappedFragment ? document2.body.innerHTML || "" : document2.toString(),
|
|
1049
|
-
unwrapped: true,
|
|
1050
|
-
unwrappedGroupId: groupId,
|
|
1051
|
-
members,
|
|
1052
|
-
groupCenter
|
|
1053
|
-
};
|
|
843
|
+
return { html: document.toString(), hostId, track, duration };
|
|
1054
844
|
}
|
|
1055
845
|
|
|
1056
846
|
// src/routes/files.ts
|
|
1057
|
-
import { parseHTML as parseHTML2 } from "linkedom";
|
|
1058
847
|
function isAcornGsapWriterEnabled() {
|
|
1059
848
|
const val = process.env["STUDIO_SDK_CUTOVER_ENABLED"];
|
|
1060
849
|
return val === "true" || val === "1";
|
|
@@ -1076,7 +865,7 @@ async function resolveProjectPath(c, adapter, pathPrefix, opts) {
|
|
|
1076
865
|
if (!absPath) {
|
|
1077
866
|
return { error: c.json({ error: "forbidden" }, 403) };
|
|
1078
867
|
}
|
|
1079
|
-
if (opts?.mustExist && !
|
|
868
|
+
if (opts?.mustExist && !existsSync4(absPath)) {
|
|
1080
869
|
return { error: c.json({ error: "not found" }, 404) };
|
|
1081
870
|
}
|
|
1082
871
|
return { project, filePath, absPath };
|
|
@@ -1087,6 +876,25 @@ function resolveProjectFile(c, adapter, opts) {
|
|
|
1087
876
|
function resolveFileMutationContext(c, adapter, operation) {
|
|
1088
877
|
return resolveProjectPath(c, adapter, (id) => `/projects/${id}/file-mutations/${operation}/`);
|
|
1089
878
|
}
|
|
879
|
+
function isAtomicCutTarget(value) {
|
|
880
|
+
if (!value || typeof value !== "object") return false;
|
|
881
|
+
const target = value;
|
|
882
|
+
return !!target.target && typeof target.target === "object" && Number.isFinite(target.splitTime) && Number.isFinite(target.elementStart) && Number.isFinite(target.elementDuration) && Number(target.elementDuration) > 0;
|
|
883
|
+
}
|
|
884
|
+
function isAtomicCutFileRequest(value) {
|
|
885
|
+
if (!value || typeof value !== "object") return false;
|
|
886
|
+
const file = value;
|
|
887
|
+
return typeof file.path === "string" && file.path.length > 0 && typeof file.expectedVersion === "string" && Array.isArray(file.targets) && file.targets.length > 0 && file.targets.every(isAtomicCutTarget);
|
|
888
|
+
}
|
|
889
|
+
var atomicCutTail = Promise.resolve();
|
|
890
|
+
function serializeAtomicCut(task) {
|
|
891
|
+
const next = atomicCutTail.then(task, task);
|
|
892
|
+
atomicCutTail = next.then(
|
|
893
|
+
() => void 0,
|
|
894
|
+
() => void 0
|
|
895
|
+
);
|
|
896
|
+
return next;
|
|
897
|
+
}
|
|
1090
898
|
function isElementPatchRequest(value) {
|
|
1091
899
|
if (typeof value !== "object" || value === null) return false;
|
|
1092
900
|
if (!("target" in value) || typeof value.target !== "object" || value.target === null) {
|
|
@@ -1122,7 +930,7 @@ function commitElementPatchBatches(projectDir, batches, writeFile = writeFileSyn
|
|
|
1122
930
|
resolvedPaths.add(absPath);
|
|
1123
931
|
let before;
|
|
1124
932
|
try {
|
|
1125
|
-
before =
|
|
933
|
+
before = readFileSync5(absPath, "utf-8");
|
|
1126
934
|
} catch {
|
|
1127
935
|
return { error: "not-found", sourceFile: batch.sourceFile };
|
|
1128
936
|
}
|
|
@@ -1234,8 +1042,8 @@ async function parseMutationBody(c) {
|
|
|
1234
1042
|
return { target: body.target, body };
|
|
1235
1043
|
}
|
|
1236
1044
|
function ensureDir(filePath) {
|
|
1237
|
-
const dir =
|
|
1238
|
-
if (!
|
|
1045
|
+
const dir = dirname2(filePath);
|
|
1046
|
+
if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
|
|
1239
1047
|
}
|
|
1240
1048
|
function generateCopyPath(projectDir, originalPath) {
|
|
1241
1049
|
const ext = originalPath.includes(".") ? "." + originalPath.split(".").pop() : "";
|
|
@@ -1244,7 +1052,7 @@ function generateCopyPath(projectDir, originalPath) {
|
|
|
1244
1052
|
const cleanBase = copyMatch ? base.slice(0, -copyMatch[0].length) : base;
|
|
1245
1053
|
let num = copyMatch ? copyMatch[1] ? parseInt(copyMatch[1]) + 1 : 2 : 1;
|
|
1246
1054
|
let candidate = num === 1 ? `${cleanBase} (copy)${ext}` : `${cleanBase} (copy ${num})${ext}`;
|
|
1247
|
-
while (
|
|
1055
|
+
while (existsSync4(resolve3(projectDir, candidate))) {
|
|
1248
1056
|
num++;
|
|
1249
1057
|
candidate = `${cleanBase} (copy ${num})${ext}`;
|
|
1250
1058
|
}
|
|
@@ -1255,7 +1063,7 @@ function walkFiles(dir, filter) {
|
|
|
1255
1063
|
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
1256
1064
|
const full = join6(dir, entry.name);
|
|
1257
1065
|
if (entry.isDirectory()) {
|
|
1258
|
-
if (entry.name === "node_modules" || entry.name === ".thumbnails" || entry.name === "renders")
|
|
1066
|
+
if (entry.name === "node_modules" || entry.name === ".thumbnails" || entry.name === "renders" || entry.name === ".transcode-cache")
|
|
1259
1067
|
continue;
|
|
1260
1068
|
results.push(...walkFiles(full, filter));
|
|
1261
1069
|
} else if (filter(entry.name)) {
|
|
@@ -1271,7 +1079,7 @@ function updateReferences(projectDir, oldPath, newPath) {
|
|
|
1271
1079
|
);
|
|
1272
1080
|
let updatedCount = 0;
|
|
1273
1081
|
for (const file of textFiles) {
|
|
1274
|
-
const content =
|
|
1082
|
+
const content = readFileSync5(file, "utf-8");
|
|
1275
1083
|
if (!content.includes(oldPath)) continue;
|
|
1276
1084
|
const updated = content.split(oldPath).join(newPath);
|
|
1277
1085
|
if (updated !== content) {
|
|
@@ -1282,10 +1090,10 @@ function updateReferences(projectDir, oldPath, newPath) {
|
|
|
1282
1090
|
return updatedCount;
|
|
1283
1091
|
}
|
|
1284
1092
|
function extractGsapScriptBlock(html) {
|
|
1285
|
-
const { document
|
|
1093
|
+
const { document } = parseHTML2(html);
|
|
1286
1094
|
const scripts = [
|
|
1287
|
-
...
|
|
1288
|
-
...Array.from(
|
|
1095
|
+
...document.querySelectorAll("script:not([src])"),
|
|
1096
|
+
...Array.from(document.querySelectorAll("template")).flatMap(
|
|
1289
1097
|
(tmpl) => Array.from(tmpl.querySelectorAll("script:not([src])"))
|
|
1290
1098
|
)
|
|
1291
1099
|
];
|
|
@@ -1294,10 +1102,10 @@ function extractGsapScriptBlock(html) {
|
|
|
1294
1102
|
if (content.includes("gsap.timeline") || content.includes(".set(") || content.includes(".to(")) {
|
|
1295
1103
|
return {
|
|
1296
1104
|
scriptText: content,
|
|
1297
|
-
document
|
|
1105
|
+
document,
|
|
1298
1106
|
replaceScript(newText) {
|
|
1299
1107
|
script.textContent = newText;
|
|
1300
|
-
return
|
|
1108
|
+
return document.toString();
|
|
1301
1109
|
}
|
|
1302
1110
|
};
|
|
1303
1111
|
}
|
|
@@ -1395,11 +1203,11 @@ function bakeGroupTransformIntoMembers(html, groupId, members, groupCenter) {
|
|
|
1395
1203
|
}
|
|
1396
1204
|
return block.replaceScript(script);
|
|
1397
1205
|
}
|
|
1398
|
-
function stripStudioEditsFromTarget(
|
|
1206
|
+
function stripStudioEditsFromTarget(document, selector) {
|
|
1399
1207
|
if (!selector) return 0;
|
|
1400
1208
|
let stripped = 0;
|
|
1401
1209
|
try {
|
|
1402
|
-
for (const el of
|
|
1210
|
+
for (const el of document.querySelectorAll(selector)) {
|
|
1403
1211
|
if (!isHTMLElement(el)) continue;
|
|
1404
1212
|
const htmlEl = el;
|
|
1405
1213
|
let touched = false;
|
|
@@ -1469,11 +1277,11 @@ function resolveFinalOpacity(anim) {
|
|
|
1469
1277
|
const num = Number(raw);
|
|
1470
1278
|
return Number.isFinite(num) && num !== 0 ? num : null;
|
|
1471
1279
|
}
|
|
1472
|
-
function bakeVisibilityOnDelete(
|
|
1280
|
+
function bakeVisibilityOnDelete(document, anim) {
|
|
1473
1281
|
const opacity = resolveFinalOpacity(anim);
|
|
1474
1282
|
if (opacity === null) return;
|
|
1475
1283
|
try {
|
|
1476
|
-
for (const el of
|
|
1284
|
+
for (const el of document.querySelectorAll(anim.targetSelector)) {
|
|
1477
1285
|
if (isHTMLElement(el)) el.style.setProperty("opacity", String(opacity));
|
|
1478
1286
|
}
|
|
1479
1287
|
} catch {
|
|
@@ -1528,7 +1336,7 @@ function validateGsapMutationRequest(c, body) {
|
|
|
1528
1336
|
return null;
|
|
1529
1337
|
}
|
|
1530
1338
|
async function prepareGsapMutationScript(c, res, firstMutation) {
|
|
1531
|
-
const beforeHtml =
|
|
1339
|
+
const beforeHtml = readFileSync5(res.absPath, "utf-8");
|
|
1532
1340
|
let html = beforeHtml;
|
|
1533
1341
|
let block = extractGsapScriptBlock(html);
|
|
1534
1342
|
if (!block && (firstMutation.type === "add" || firstMutation.type === "add-with-keyframes")) {
|
|
@@ -1588,7 +1396,7 @@ async function applyGsapMutations(c, res, mutations) {
|
|
|
1588
1396
|
const changed = block.scriptText !== initialScript;
|
|
1589
1397
|
const newHtml = changed ? block.replaceScript(block.scriptText) : html;
|
|
1590
1398
|
let backupPath = null;
|
|
1591
|
-
if (
|
|
1399
|
+
if (readFileSync5(res.absPath, "utf-8") !== beforeHtml) {
|
|
1592
1400
|
return c.json({ error: "file changed during GSAP mutation", conflict: true }, 409);
|
|
1593
1401
|
}
|
|
1594
1402
|
if (changed) {
|
|
@@ -2211,6 +2019,74 @@ async function executeGsapMutationRecast(body, block, respond) {
|
|
|
2211
2019
|
return respond({ error: `unknown mutation type: ${body.type}` }, 400);
|
|
2212
2020
|
}
|
|
2213
2021
|
}
|
|
2022
|
+
async function foldAtomicCutFile(c, file, absPath, before) {
|
|
2023
|
+
let after = before;
|
|
2024
|
+
let splitCount = 0;
|
|
2025
|
+
const skippedSelectors = /* @__PURE__ */ new Set();
|
|
2026
|
+
const respond = (data, status) => status ? c.json(data, status) : c.json(data);
|
|
2027
|
+
const orderedTargets = file.targets.map((cut, index) => ({ cut, index })).sort((left, right) => {
|
|
2028
|
+
const locatorKey = (entry) => !entry.target.id && !entry.target.hfId && entry.target.selector ? entry.target.selector : null;
|
|
2029
|
+
const leftKey = locatorKey(left.cut);
|
|
2030
|
+
const rightKey = locatorKey(right.cut);
|
|
2031
|
+
if (leftKey && rightKey) {
|
|
2032
|
+
return leftKey.localeCompare(rightKey) || (right.cut.target.selectorIndex ?? 0) - (left.cut.target.selectorIndex ?? 0);
|
|
2033
|
+
}
|
|
2034
|
+
if (leftKey) return -1;
|
|
2035
|
+
if (rightKey) return 1;
|
|
2036
|
+
return left.index - right.index;
|
|
2037
|
+
}).map(({ cut }) => cut);
|
|
2038
|
+
for (const cut of orderedTargets) {
|
|
2039
|
+
const baseId = cut.originalId || cut.target.id || "clip";
|
|
2040
|
+
const split = splitElementInHtml(after, cut.target, cut.splitTime, `${baseId}-split`, {
|
|
2041
|
+
start: cut.elementStart,
|
|
2042
|
+
duration: cut.elementDuration,
|
|
2043
|
+
playbackStart: cut.playbackStart,
|
|
2044
|
+
playbackRate: cut.playbackRate,
|
|
2045
|
+
stampPlaybackStart: cut.isComposition
|
|
2046
|
+
});
|
|
2047
|
+
if (!split.matched || !split.newId) {
|
|
2048
|
+
return c.json(
|
|
2049
|
+
{ error: `Cut target was not found or was outside its authored bounds in ${file.path}` },
|
|
2050
|
+
400
|
|
2051
|
+
);
|
|
2052
|
+
}
|
|
2053
|
+
after = split.html;
|
|
2054
|
+
splitCount++;
|
|
2055
|
+
if (!cut.originalId) continue;
|
|
2056
|
+
const block = extractGsapScriptBlock(after);
|
|
2057
|
+
if (!block) continue;
|
|
2058
|
+
const result = await executeGsapMutation(
|
|
2059
|
+
{
|
|
2060
|
+
type: "split-animations",
|
|
2061
|
+
originalId: cut.originalId,
|
|
2062
|
+
newId: split.newId,
|
|
2063
|
+
splitTime: cut.splitTime,
|
|
2064
|
+
elementStart: cut.elementStart,
|
|
2065
|
+
elementDuration: cut.elementDuration
|
|
2066
|
+
},
|
|
2067
|
+
block,
|
|
2068
|
+
respond
|
|
2069
|
+
);
|
|
2070
|
+
if (result instanceof Response) return result;
|
|
2071
|
+
let script = typeof result === "string" ? result : result.script;
|
|
2072
|
+
if (typeof result !== "string") {
|
|
2073
|
+
for (const selector of result.skippedSelectors) skippedSelectors.add(selector);
|
|
2074
|
+
}
|
|
2075
|
+
if (script !== block.scriptText) {
|
|
2076
|
+
const parser = await loadGsapParser();
|
|
2077
|
+
script = parser.syncPositionHoldsBeforeKeyframes(script);
|
|
2078
|
+
after = block.replaceScript(script);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
return {
|
|
2082
|
+
path: file.path,
|
|
2083
|
+
absPath,
|
|
2084
|
+
before,
|
|
2085
|
+
after,
|
|
2086
|
+
splitCount,
|
|
2087
|
+
skippedSelectors: [...skippedSelectors]
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2214
2090
|
async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
2215
2091
|
const MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
|
|
2216
2092
|
const uploaded = [];
|
|
@@ -2230,13 +2106,13 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
2230
2106
|
if (!isSafePath(projectDir, destPath)) continue;
|
|
2231
2107
|
let finalPath = destPath;
|
|
2232
2108
|
let finalName = name;
|
|
2233
|
-
if (
|
|
2109
|
+
if (existsSync4(finalPath)) {
|
|
2234
2110
|
const dotIdx = name.indexOf(".", name.startsWith(".") ? 1 : 0);
|
|
2235
2111
|
const ext = dotIdx > 0 ? name.slice(dotIdx) : "";
|
|
2236
2112
|
const base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
|
|
2237
2113
|
let n = 2;
|
|
2238
2114
|
const MAX_COPY_INDEX = 1e4;
|
|
2239
|
-
while (n < MAX_COPY_INDEX &&
|
|
2115
|
+
while (n < MAX_COPY_INDEX && existsSync4(resolve3(targetDir, `${base} (${n})${ext}`))) n++;
|
|
2240
2116
|
if (n >= MAX_COPY_INDEX) {
|
|
2241
2117
|
skipped.push(name);
|
|
2242
2118
|
continue;
|
|
@@ -2264,13 +2140,13 @@ function registerFileRoutes(api, adapter) {
|
|
|
2264
2140
|
api.get("/projects/:id/files/*", async (c) => {
|
|
2265
2141
|
const res = await resolveProjectFile(c, adapter);
|
|
2266
2142
|
if ("error" in res) return res.error;
|
|
2267
|
-
if (!
|
|
2143
|
+
if (!existsSync4(res.absPath)) {
|
|
2268
2144
|
if (c.req.query("optional") === "1") {
|
|
2269
2145
|
return c.json({ filename: res.filePath, content: "" });
|
|
2270
2146
|
}
|
|
2271
2147
|
return c.json({ error: "not found" }, 404);
|
|
2272
2148
|
}
|
|
2273
|
-
const content =
|
|
2149
|
+
const content = readFileSync5(res.absPath, "utf-8");
|
|
2274
2150
|
const version = fileContentVersion(content);
|
|
2275
2151
|
c.header("ETag", version);
|
|
2276
2152
|
return c.json({ filename: res.filePath, content, version });
|
|
@@ -2284,7 +2160,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2284
2160
|
if (expectedVersion === null && !createOnly) {
|
|
2285
2161
|
let currentContent = null;
|
|
2286
2162
|
try {
|
|
2287
|
-
currentContent =
|
|
2163
|
+
currentContent = readFileSync5(res.absPath, "utf-8");
|
|
2288
2164
|
} catch (error) {
|
|
2289
2165
|
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
|
|
2290
2166
|
throw error;
|
|
@@ -2310,7 +2186,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2310
2186
|
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
|
|
2311
2187
|
throw error;
|
|
2312
2188
|
}
|
|
2313
|
-
const currentContent =
|
|
2189
|
+
const currentContent = readFileSync5(res.absPath, "utf-8");
|
|
2314
2190
|
return c.json(
|
|
2315
2191
|
{
|
|
2316
2192
|
error: "file conflict",
|
|
@@ -2345,7 +2221,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2345
2221
|
);
|
|
2346
2222
|
}
|
|
2347
2223
|
try {
|
|
2348
|
-
const currentContent =
|
|
2224
|
+
const currentContent = readFileSync5(fd, "utf-8");
|
|
2349
2225
|
const currentVersion = fileContentVersion(currentContent);
|
|
2350
2226
|
if (expectedVersion !== currentVersion) {
|
|
2351
2227
|
return c.json(
|
|
@@ -2382,7 +2258,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2382
2258
|
api.post("/projects/:id/files/*", async (c) => {
|
|
2383
2259
|
const res = await resolveProjectFile(c, adapter);
|
|
2384
2260
|
if ("error" in res) return res.error;
|
|
2385
|
-
if (
|
|
2261
|
+
if (existsSync4(res.absPath)) {
|
|
2386
2262
|
return c.json({ error: "already exists" }, 409);
|
|
2387
2263
|
}
|
|
2388
2264
|
ensureDir(res.absPath);
|
|
@@ -2406,15 +2282,71 @@ function registerFileRoutes(api, adapter) {
|
|
|
2406
2282
|
backupPath: backupPathForResponse(res.project.dir, backup.backupPath)
|
|
2407
2283
|
});
|
|
2408
2284
|
});
|
|
2285
|
+
api.post("/projects/:id/file-mutations/insert-composition/*", async (c) => {
|
|
2286
|
+
const ctx = await resolveFileMutationContext(c, adapter, "insert-composition");
|
|
2287
|
+
if ("error" in ctx) return ctx.error;
|
|
2288
|
+
const body = await c.req.json().catch(() => null);
|
|
2289
|
+
if (!body || typeof body.sourcePath !== "string" || typeof body.start !== "number" || !Number.isFinite(body.start) || body.start < 0 || typeof body.track !== "number" || !Number.isFinite(body.track) || typeof body.expectedVersion !== "string") {
|
|
2290
|
+
return c.json({ error: "sourcePath, finite placement, and expectedVersion required" }, 400);
|
|
2291
|
+
}
|
|
2292
|
+
let before;
|
|
2293
|
+
try {
|
|
2294
|
+
before = readFileSync5(ctx.absPath, "utf-8");
|
|
2295
|
+
} catch (error) {
|
|
2296
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
|
|
2297
|
+
throw error;
|
|
2298
|
+
}
|
|
2299
|
+
return c.json({ error: "not found" }, 404);
|
|
2300
|
+
}
|
|
2301
|
+
const currentVersion = fileContentVersion(before);
|
|
2302
|
+
if (body.expectedVersion !== currentVersion) {
|
|
2303
|
+
return c.json({ error: "file conflict", currentVersion, currentContent: before }, 409);
|
|
2304
|
+
}
|
|
2305
|
+
let insertion;
|
|
2306
|
+
try {
|
|
2307
|
+
insertion = insertCompositionIntoSource({
|
|
2308
|
+
projectDir: ctx.project.dir,
|
|
2309
|
+
targetPath: ctx.filePath,
|
|
2310
|
+
sourcePath: body.sourcePath,
|
|
2311
|
+
parentSource: before,
|
|
2312
|
+
start: body.start,
|
|
2313
|
+
desiredTrack: body.track
|
|
2314
|
+
});
|
|
2315
|
+
} catch (error) {
|
|
2316
|
+
if (error instanceof CompositionInsertionError) {
|
|
2317
|
+
return c.json({ error: error.message }, error.status);
|
|
2318
|
+
}
|
|
2319
|
+
throw error;
|
|
2320
|
+
}
|
|
2321
|
+
const backup = snapshotBeforeWrite(ctx.project.dir, ctx.absPath);
|
|
2322
|
+
if (backup.error) return c.json({ error: `backup failed: ${backup.error}` }, 500);
|
|
2323
|
+
writeFileSync4(ctx.absPath, insertion.html, "utf-8");
|
|
2324
|
+
const version = fileContentVersion(insertion.html);
|
|
2325
|
+
const writeToken = createWriteToken(c.req.header("X-Hyperframes-Write-Token"));
|
|
2326
|
+
recordFileWriteReceipt(ctx.absPath, { path: ctx.filePath, version, writeToken });
|
|
2327
|
+
c.header("ETag", version);
|
|
2328
|
+
return c.json({
|
|
2329
|
+
ok: true,
|
|
2330
|
+
path: ctx.filePath,
|
|
2331
|
+
hostId: insertion.hostId,
|
|
2332
|
+
track: insertion.track,
|
|
2333
|
+
duration: insertion.duration,
|
|
2334
|
+
before,
|
|
2335
|
+
after: insertion.html,
|
|
2336
|
+
version,
|
|
2337
|
+
writeToken,
|
|
2338
|
+
backupPath: backupPathForResponse(ctx.project.dir, backup.backupPath)
|
|
2339
|
+
});
|
|
2340
|
+
});
|
|
2409
2341
|
api.post("/projects/:id/file-mutations/remove-element/*", async (c) => {
|
|
2410
2342
|
const ctx = await resolveFileMutationContext(c, adapter, "remove-element");
|
|
2411
2343
|
if ("error" in ctx) return ctx.error;
|
|
2412
|
-
if (!
|
|
2344
|
+
if (!existsSync4(ctx.absPath)) {
|
|
2413
2345
|
return c.json({ error: "not found" }, 404);
|
|
2414
2346
|
}
|
|
2415
2347
|
const parsed = await parseMutationBody(c);
|
|
2416
2348
|
if ("error" in parsed) return parsed.error;
|
|
2417
|
-
const originalContent =
|
|
2349
|
+
const originalContent = readFileSync5(ctx.absPath, "utf-8");
|
|
2418
2350
|
return writeIfChanged(
|
|
2419
2351
|
c,
|
|
2420
2352
|
ctx.project.dir,
|
|
@@ -2424,6 +2356,130 @@ function registerFileRoutes(api, adapter) {
|
|
|
2424
2356
|
removeElementFromHtml(originalContent, parsed.target)
|
|
2425
2357
|
);
|
|
2426
2358
|
});
|
|
2359
|
+
api.post("/projects/:id/file-mutations/split-batch", async (c) => {
|
|
2360
|
+
const body = await c.req.json().catch(() => null);
|
|
2361
|
+
if (!Array.isArray(body?.files) || body.files.length === 0 || !body.files.every(isAtomicCutFileRequest)) {
|
|
2362
|
+
return c.json({ error: "files with path, expectedVersion, and cut targets required" }, 400);
|
|
2363
|
+
}
|
|
2364
|
+
const files = body.files;
|
|
2365
|
+
const project = await adapter.resolveProject(c.req.param("id"));
|
|
2366
|
+
if (!project) return c.json({ error: "not found" }, 404);
|
|
2367
|
+
return serializeAtomicCut(async () => {
|
|
2368
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2369
|
+
const prepared = [];
|
|
2370
|
+
for (const file of files) {
|
|
2371
|
+
const absPath = resolveWithinProject(project.dir, file.path);
|
|
2372
|
+
if (!absPath) return c.json({ error: `forbidden path: ${file.path}` }, 403);
|
|
2373
|
+
if (seen.has(absPath)) return c.json({ error: `duplicate path: ${file.path}` }, 400);
|
|
2374
|
+
seen.add(absPath);
|
|
2375
|
+
let before;
|
|
2376
|
+
try {
|
|
2377
|
+
before = readFileSync5(absPath, "utf-8");
|
|
2378
|
+
} catch {
|
|
2379
|
+
return c.json({ error: `not found: ${file.path}` }, 404);
|
|
2380
|
+
}
|
|
2381
|
+
const currentVersion = fileContentVersion(before);
|
|
2382
|
+
if (currentVersion !== file.expectedVersion) {
|
|
2383
|
+
return c.json(
|
|
2384
|
+
{
|
|
2385
|
+
error: `file conflict: ${file.path}`,
|
|
2386
|
+
path: file.path,
|
|
2387
|
+
currentVersion,
|
|
2388
|
+
currentContent: before
|
|
2389
|
+
},
|
|
2390
|
+
409
|
|
2391
|
+
);
|
|
2392
|
+
}
|
|
2393
|
+
let folded;
|
|
2394
|
+
try {
|
|
2395
|
+
folded = await foldAtomicCutFile(c, file, absPath, before);
|
|
2396
|
+
} catch (error) {
|
|
2397
|
+
const message = error instanceof Error ? error.message : "Cut transform failed";
|
|
2398
|
+
return c.json({ error: message }, 400);
|
|
2399
|
+
}
|
|
2400
|
+
if (folded instanceof Response) return folded;
|
|
2401
|
+
prepared.push(folded);
|
|
2402
|
+
}
|
|
2403
|
+
for (const file of prepared) {
|
|
2404
|
+
const current = readFileSync5(file.absPath, "utf-8");
|
|
2405
|
+
if (current !== file.before) {
|
|
2406
|
+
return c.json(
|
|
2407
|
+
{
|
|
2408
|
+
error: `file conflict: ${file.path}`,
|
|
2409
|
+
path: file.path,
|
|
2410
|
+
currentVersion: fileContentVersion(current),
|
|
2411
|
+
currentContent: current
|
|
2412
|
+
},
|
|
2413
|
+
409
|
|
2414
|
+
);
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
const backups = /* @__PURE__ */ new Map();
|
|
2418
|
+
for (const file of prepared) {
|
|
2419
|
+
const backup = snapshotBeforeWrite(project.dir, file.absPath);
|
|
2420
|
+
if (backup.error) {
|
|
2421
|
+
return c.json(
|
|
2422
|
+
{ error: `Failed to create backup for ${file.path}: ${backup.error}` },
|
|
2423
|
+
500
|
|
2424
|
+
);
|
|
2425
|
+
}
|
|
2426
|
+
backups.set(file.path, backupPathForResponse(project.dir, backup.backupPath));
|
|
2427
|
+
}
|
|
2428
|
+
const writeToken = createWriteToken(
|
|
2429
|
+
typeof body.transactionToken === "string" ? body.transactionToken : c.req.header("X-Hyperframes-Write-Token")
|
|
2430
|
+
);
|
|
2431
|
+
const written = [];
|
|
2432
|
+
try {
|
|
2433
|
+
for (const file of prepared) {
|
|
2434
|
+
writeFileSync4(file.absPath, file.after, "utf-8");
|
|
2435
|
+
written.push(file);
|
|
2436
|
+
recordFileWriteReceipt(file.absPath, {
|
|
2437
|
+
path: file.path,
|
|
2438
|
+
version: fileContentVersion(file.after),
|
|
2439
|
+
writeToken
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
} catch (error) {
|
|
2443
|
+
const conflicts = [];
|
|
2444
|
+
for (const file of written.reverse()) {
|
|
2445
|
+
try {
|
|
2446
|
+
const current = readFileSync5(file.absPath, "utf-8");
|
|
2447
|
+
if (current !== file.after) {
|
|
2448
|
+
conflicts.push(file.path);
|
|
2449
|
+
continue;
|
|
2450
|
+
}
|
|
2451
|
+
writeFileSync4(file.absPath, file.before, "utf-8");
|
|
2452
|
+
recordFileWriteReceipt(file.absPath, {
|
|
2453
|
+
path: file.path,
|
|
2454
|
+
version: fileContentVersion(file.before),
|
|
2455
|
+
writeToken
|
|
2456
|
+
});
|
|
2457
|
+
} catch {
|
|
2458
|
+
conflicts.push(file.path);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
return c.json(
|
|
2462
|
+
{
|
|
2463
|
+
error: error instanceof Error ? error.message : "Cut write failed",
|
|
2464
|
+
outcome: conflicts.length ? "aborted-with-conflicts" : "aborted-restored",
|
|
2465
|
+
conflicts
|
|
2466
|
+
},
|
|
2467
|
+
conflicts.length ? 409 : 500
|
|
2468
|
+
);
|
|
2469
|
+
}
|
|
2470
|
+
const result = prepared.map((file) => ({
|
|
2471
|
+
path: file.path,
|
|
2472
|
+
before: file.before,
|
|
2473
|
+
after: file.after,
|
|
2474
|
+
version: fileContentVersion(file.after),
|
|
2475
|
+
writeToken,
|
|
2476
|
+
backupPath: backups.get(file.path) ?? null,
|
|
2477
|
+
splitCount: file.splitCount,
|
|
2478
|
+
skippedSelectors: file.skippedSelectors
|
|
2479
|
+
}));
|
|
2480
|
+
return c.json({ ok: true, outcome: "committed", files: result });
|
|
2481
|
+
});
|
|
2482
|
+
});
|
|
2427
2483
|
api.post("/projects/:id/file-mutations/split-element/*", async (c) => {
|
|
2428
2484
|
const ctx = await resolveFileMutationContext(c, adapter, "split-element");
|
|
2429
2485
|
if ("error" in ctx) return ctx.error;
|
|
@@ -2435,7 +2491,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2435
2491
|
const fallbackTiming = typeof parsed.body.elementStart === "number" && typeof parsed.body.elementDuration === "number" ? { start: parsed.body.elementStart, duration: parsed.body.elementDuration } : void 0;
|
|
2436
2492
|
let originalContent;
|
|
2437
2493
|
try {
|
|
2438
|
-
originalContent =
|
|
2494
|
+
originalContent = readFileSync5(ctx.absPath, "utf-8");
|
|
2439
2495
|
} catch {
|
|
2440
2496
|
return c.json({ error: "not found" }, 404);
|
|
2441
2497
|
}
|
|
@@ -2486,7 +2542,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2486
2542
|
}
|
|
2487
2543
|
let originalContent;
|
|
2488
2544
|
try {
|
|
2489
|
-
originalContent =
|
|
2545
|
+
originalContent = readFileSync5(ctx.absPath, "utf-8");
|
|
2490
2546
|
} catch {
|
|
2491
2547
|
return c.json({ error: "not found" }, 404);
|
|
2492
2548
|
}
|
|
@@ -2576,7 +2632,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2576
2632
|
}
|
|
2577
2633
|
let originalContent;
|
|
2578
2634
|
try {
|
|
2579
|
-
originalContent =
|
|
2635
|
+
originalContent = readFileSync5(ctx.absPath, "utf-8");
|
|
2580
2636
|
} catch {
|
|
2581
2637
|
return c.json({ error: "not found" }, 404);
|
|
2582
2638
|
}
|
|
@@ -2618,7 +2674,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2618
2674
|
if ("error" in parsed) return parsed.error;
|
|
2619
2675
|
let originalContent;
|
|
2620
2676
|
try {
|
|
2621
|
-
originalContent =
|
|
2677
|
+
originalContent = readFileSync5(ctx.absPath, "utf-8");
|
|
2622
2678
|
} catch {
|
|
2623
2679
|
return c.json({ error: "not found" }, 404);
|
|
2624
2680
|
}
|
|
@@ -2647,7 +2703,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2647
2703
|
if ("error" in parsed) return parsed.error;
|
|
2648
2704
|
let content;
|
|
2649
2705
|
try {
|
|
2650
|
-
content =
|
|
2706
|
+
content = readFileSync5(ctx.absPath, "utf-8");
|
|
2651
2707
|
} catch {
|
|
2652
2708
|
return c.json({ exists: false });
|
|
2653
2709
|
}
|
|
@@ -2665,7 +2721,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2665
2721
|
if (!newAbs) {
|
|
2666
2722
|
return c.json({ error: "forbidden" }, 403);
|
|
2667
2723
|
}
|
|
2668
|
-
if (
|
|
2724
|
+
if (existsSync4(newAbs)) {
|
|
2669
2725
|
return c.json({ error: "already exists" }, 409);
|
|
2670
2726
|
}
|
|
2671
2727
|
ensureDir(newAbs);
|
|
@@ -2681,7 +2737,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2681
2737
|
return c.json({ error: "path required" }, 400);
|
|
2682
2738
|
}
|
|
2683
2739
|
const srcAbs = resolveWithinProject(project.dir, body.path);
|
|
2684
|
-
if (!srcAbs || !
|
|
2740
|
+
if (!srcAbs || !existsSync4(srcAbs)) {
|
|
2685
2741
|
return c.json({ error: "not found" }, 404);
|
|
2686
2742
|
}
|
|
2687
2743
|
const copyPath = generateCopyPath(project.dir, body.path);
|
|
@@ -2690,7 +2746,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2690
2746
|
return c.json({ error: "forbidden" }, 403);
|
|
2691
2747
|
}
|
|
2692
2748
|
ensureDir(destAbs);
|
|
2693
|
-
writeFileSync4(destAbs,
|
|
2749
|
+
writeFileSync4(destAbs, readFileSync5(srcAbs));
|
|
2694
2750
|
return c.json({ ok: true, path: copyPath }, 201);
|
|
2695
2751
|
});
|
|
2696
2752
|
const MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
|
|
@@ -2706,7 +2762,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2706
2762
|
const subDir = c.req.query("dir") ?? "";
|
|
2707
2763
|
const targetDir = subDir ? resolveWithinProject(project.dir, subDir) : project.dir;
|
|
2708
2764
|
if (!targetDir) return c.json({ error: "forbidden" }, 403);
|
|
2709
|
-
if (subDir && !
|
|
2765
|
+
if (subDir && !existsSync4(targetDir)) mkdirSync3(targetDir, { recursive: true });
|
|
2710
2766
|
const formData = await c.req.formData();
|
|
2711
2767
|
const result = await processUploadedFiles(formData, targetDir, project.dir);
|
|
2712
2768
|
return c.json(
|
|
@@ -2720,7 +2776,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2720
2776
|
mustExist: true
|
|
2721
2777
|
});
|
|
2722
2778
|
if ("error" in res) return res.error;
|
|
2723
|
-
const html =
|
|
2779
|
+
const html = readFileSync5(res.absPath, "utf-8");
|
|
2724
2780
|
const block = extractGsapScriptBlock(html);
|
|
2725
2781
|
if (!block) {
|
|
2726
2782
|
return c.json({
|
|
@@ -2779,7 +2835,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2779
2835
|
if (!body || typeof body.expected !== "string" || typeof body.restore !== "string") {
|
|
2780
2836
|
return c.json({ error: "expected and restore contents required" }, 400);
|
|
2781
2837
|
}
|
|
2782
|
-
const current =
|
|
2838
|
+
const current = readFileSync5(res.absPath, "utf-8");
|
|
2783
2839
|
if (current !== body.expected) {
|
|
2784
2840
|
return c.json({ ok: true, restored: false, conflict: true });
|
|
2785
2841
|
}
|
|
@@ -2789,13 +2845,13 @@ function registerFileRoutes(api, adapter) {
|
|
|
2789
2845
|
}
|
|
2790
2846
|
|
|
2791
2847
|
// src/routes/preview.ts
|
|
2792
|
-
import { existsSync as
|
|
2848
|
+
import { existsSync as existsSync6, readFileSync as readFileSync8, statSync as statSync2 } from "fs";
|
|
2793
2849
|
import { join as join8 } from "path";
|
|
2794
2850
|
import { createHash as createHash3 } from "crypto";
|
|
2795
2851
|
import { injectScriptsIntoHtml, stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2 } from "@hyperframes/core/compiler";
|
|
2796
2852
|
|
|
2797
2853
|
// src/helpers/subComposition.ts
|
|
2798
|
-
import { existsSync as
|
|
2854
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
|
|
2799
2855
|
import { join as join7 } from "path";
|
|
2800
2856
|
import { parseHTML as parseHTML3 } from "linkedom";
|
|
2801
2857
|
import {
|
|
@@ -2903,8 +2959,8 @@ function tagRootCompositionFile(bodyHtml, compPath) {
|
|
|
2903
2959
|
}
|
|
2904
2960
|
function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, rawOverride) {
|
|
2905
2961
|
const compFile = join7(projectDir, compPath);
|
|
2906
|
-
if (!
|
|
2907
|
-
const rawComp = rawOverride ??
|
|
2962
|
+
if (!existsSync5(compFile)) return null;
|
|
2963
|
+
const rawComp = rawOverride ?? readFileSync6(compFile, "utf-8");
|
|
2908
2964
|
let compHeadContent = "";
|
|
2909
2965
|
let rewrittenContent;
|
|
2910
2966
|
let htmlAttrs = "";
|
|
@@ -2936,8 +2992,8 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
|
|
|
2936
2992
|
rewrittenContent = tagRootCompositionFile(rewrittenContent, compPath);
|
|
2937
2993
|
const indexPath = join7(projectDir, "index.html");
|
|
2938
2994
|
let headContent = "";
|
|
2939
|
-
if (
|
|
2940
|
-
const indexHtml =
|
|
2995
|
+
if (existsSync5(indexPath)) {
|
|
2996
|
+
const indexHtml = readFileSync6(indexPath, "utf-8");
|
|
2941
2997
|
const headMatch = indexHtml.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
|
|
2942
2998
|
headContent = headMatch?.[1] ?? "";
|
|
2943
2999
|
}
|
|
@@ -2970,215 +3026,28 @@ ${rewrittenContent}
|
|
|
2970
3026
|
</html>`;
|
|
2971
3027
|
}
|
|
2972
3028
|
|
|
2973
|
-
// src/helpers/studioMotionRenderScript.ts
|
|
2974
|
-
var STUDIO_MOTION_PATH = ".hyperframes/studio-motion.json";
|
|
2975
|
-
function hasStudioMotionEntries(manifestContent) {
|
|
2976
|
-
try {
|
|
2977
|
-
const parsed = JSON.parse(manifestContent);
|
|
2978
|
-
return Array.isArray(parsed.motions) && parsed.motions.length > 0;
|
|
2979
|
-
} catch {
|
|
2980
|
-
return false;
|
|
2981
|
-
}
|
|
2982
|
-
}
|
|
2983
|
-
function createStudioMotionRenderBodyScript(manifestContent, options = {}) {
|
|
2984
|
-
if (!manifestContent.trim() || !hasStudioMotionEntries(manifestContent)) return null;
|
|
2985
|
-
return `(${studioMotionRenderRuntime.toString()})(${JSON.stringify(manifestContent)}, ${JSON.stringify(options.activeCompositionPath ?? null)});`;
|
|
2986
|
-
}
|
|
2987
|
-
function studioMotionRenderRuntime(manifestContent, activeCompositionPath) {
|
|
2988
|
-
const STUDIO_MOTION_TIMELINE_ID = "studio-motion";
|
|
2989
|
-
const STUDIO_MOTION_ATTR = "data-hf-studio-motion";
|
|
2990
|
-
const ORIGINAL_TRANSFORM_ATTR = "data-hf-studio-motion-original-transform";
|
|
2991
|
-
const ORIGINAL_OPACITY_ATTR = "data-hf-studio-motion-original-opacity";
|
|
2992
|
-
const ORIGINAL_VISIBILITY_ATTR = "data-hf-studio-motion-original-visibility";
|
|
2993
|
-
const objectRecord = (value) => value && typeof value === "object" ? value : null;
|
|
2994
|
-
const finiteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
2995
|
-
const runtimeWindow = window;
|
|
2996
|
-
const parseMotionValues = (value) => {
|
|
2997
|
-
const record = objectRecord(value);
|
|
2998
|
-
if (!record) return null;
|
|
2999
|
-
const parsed = {};
|
|
3000
|
-
for (const key of ["x", "y", "scale", "rotation", "opacity", "autoAlpha"]) {
|
|
3001
|
-
const next = finiteNumber(record[key]);
|
|
3002
|
-
if (next != null) parsed[key] = next;
|
|
3003
|
-
}
|
|
3004
|
-
return Object.keys(parsed).length > 0 ? parsed : null;
|
|
3005
|
-
};
|
|
3006
|
-
const parseCustomEase = (value) => {
|
|
3007
|
-
const record = objectRecord(value);
|
|
3008
|
-
if (!record) return null;
|
|
3009
|
-
const id = typeof record.id === "string" ? record.id.trim() : "";
|
|
3010
|
-
const data = typeof record.data === "string" ? record.data.trim() : "";
|
|
3011
|
-
if (!id || !data) return null;
|
|
3012
|
-
return { id, data };
|
|
3013
|
-
};
|
|
3014
|
-
const parsedManifest = (() => {
|
|
3015
|
-
try {
|
|
3016
|
-
return objectRecord(JSON.parse(manifestContent));
|
|
3017
|
-
} catch {
|
|
3018
|
-
return null;
|
|
3019
|
-
}
|
|
3020
|
-
})();
|
|
3021
|
-
const manifestMotions = Array.isArray(parsedManifest?.motions) ? parsedManifest.motions : [];
|
|
3022
|
-
const sourceFileForElement = (element) => {
|
|
3023
|
-
let current = element;
|
|
3024
|
-
while (current) {
|
|
3025
|
-
const sourceFile = current.getAttribute("data-composition-file") ?? current.getAttribute("data-composition-src");
|
|
3026
|
-
if (sourceFile) return sourceFile;
|
|
3027
|
-
current = current.parentElement;
|
|
3028
|
-
}
|
|
3029
|
-
return activeCompositionPath ?? "index.html";
|
|
3030
|
-
};
|
|
3031
|
-
const elementMatchesSourceFile = (element, sourceFile) => sourceFileForElement(element) === sourceFile;
|
|
3032
|
-
const isHTMLElement2 = (element) => element instanceof HTMLElement;
|
|
3033
|
-
const querySelectorCandidates = (selector) => {
|
|
3034
|
-
const className = selector.match(/^\.([A-Za-z0-9_-]+)$/)?.[1];
|
|
3035
|
-
if (className) {
|
|
3036
|
-
return Array.from(document.getElementsByTagName("*")).filter(
|
|
3037
|
-
(element) => isHTMLElement2(element) && element.classList.contains(className)
|
|
3038
|
-
);
|
|
3039
|
-
}
|
|
3040
|
-
if (/^[A-Za-z][A-Za-z0-9-]*$/.test(selector)) {
|
|
3041
|
-
return Array.from(document.getElementsByTagName(selector)).filter(isHTMLElement2);
|
|
3042
|
-
}
|
|
3043
|
-
return Array.from(document.querySelectorAll(selector)).filter(isHTMLElement2);
|
|
3044
|
-
};
|
|
3045
|
-
const resolveTarget = (targetRecord) => {
|
|
3046
|
-
const sourceFile = typeof targetRecord.sourceFile === "string" ? targetRecord.sourceFile : "";
|
|
3047
|
-
if (!sourceFile) return null;
|
|
3048
|
-
const id = typeof targetRecord.id === "string" ? targetRecord.id : "";
|
|
3049
|
-
if (id) {
|
|
3050
|
-
const byId = document.getElementById(id);
|
|
3051
|
-
if (isHTMLElement2(byId) && elementMatchesSourceFile(byId, sourceFile)) return byId;
|
|
3052
|
-
}
|
|
3053
|
-
const selector = typeof targetRecord.selector === "string" ? targetRecord.selector : "";
|
|
3054
|
-
if (!selector) return null;
|
|
3055
|
-
try {
|
|
3056
|
-
const selectorIndex = Math.max(0, Math.floor(finiteNumber(targetRecord.selectorIndex) ?? 0));
|
|
3057
|
-
return querySelectorCandidates(selector).filter(
|
|
3058
|
-
(element) => elementMatchesSourceFile(element, sourceFile)
|
|
3059
|
-
)[selectorIndex] ?? null;
|
|
3060
|
-
} catch {
|
|
3061
|
-
return null;
|
|
3062
|
-
}
|
|
3063
|
-
};
|
|
3064
|
-
const restoreElement = (element) => {
|
|
3065
|
-
runtimeWindow.gsap?.set?.(element, { clearProps: "transform,opacity,visibility" });
|
|
3066
|
-
element.style.transform = element.getAttribute(ORIGINAL_TRANSFORM_ATTR) ?? "";
|
|
3067
|
-
element.style.opacity = element.getAttribute(ORIGINAL_OPACITY_ATTR) ?? "";
|
|
3068
|
-
element.style.visibility = element.getAttribute(ORIGINAL_VISIBILITY_ATTR) ?? "";
|
|
3069
|
-
element.removeAttribute(STUDIO_MOTION_ATTR);
|
|
3070
|
-
element.removeAttribute(ORIGINAL_TRANSFORM_ATTR);
|
|
3071
|
-
element.removeAttribute(ORIGINAL_OPACITY_ATTR);
|
|
3072
|
-
element.removeAttribute(ORIGINAL_VISIBILITY_ATTR);
|
|
3073
|
-
};
|
|
3074
|
-
const restoreStudioMotionElements = () => {
|
|
3075
|
-
for (const element of Array.from(document.querySelectorAll(`[${STUDIO_MOTION_ATTR}]`))) {
|
|
3076
|
-
if (isHTMLElement2(element)) restoreElement(element);
|
|
3077
|
-
}
|
|
3078
|
-
};
|
|
3079
|
-
const readCurrentTime = () => {
|
|
3080
|
-
try {
|
|
3081
|
-
const playerTime = runtimeWindow.__player?.getTime?.();
|
|
3082
|
-
if (typeof playerTime === "number" && Number.isFinite(playerTime)) {
|
|
3083
|
-
return Math.max(0, playerTime);
|
|
3084
|
-
}
|
|
3085
|
-
} catch {
|
|
3086
|
-
}
|
|
3087
|
-
try {
|
|
3088
|
-
const timelineTime = runtimeWindow.__timeline?.time?.();
|
|
3089
|
-
if (typeof timelineTime === "number" && Number.isFinite(timelineTime)) {
|
|
3090
|
-
return Math.max(0, timelineTime);
|
|
3091
|
-
}
|
|
3092
|
-
} catch {
|
|
3093
|
-
}
|
|
3094
|
-
return 0;
|
|
3095
|
-
};
|
|
3096
|
-
const resolveEase = (motion) => {
|
|
3097
|
-
const fallback = typeof motion.ease === "string" && motion.ease.trim() ? motion.ease.trim() : "none";
|
|
3098
|
-
const customEase = parseCustomEase(motion.customEase);
|
|
3099
|
-
const customEasePlugin = runtimeWindow.CustomEase;
|
|
3100
|
-
if (!customEase || typeof customEasePlugin?.create !== "function") return fallback;
|
|
3101
|
-
try {
|
|
3102
|
-
runtimeWindow.gsap?.registerPlugin?.(customEasePlugin);
|
|
3103
|
-
customEasePlugin.create(customEase.id, customEase.data);
|
|
3104
|
-
return customEase.id;
|
|
3105
|
-
} catch {
|
|
3106
|
-
return fallback;
|
|
3107
|
-
}
|
|
3108
|
-
};
|
|
3109
|
-
const applyManifest = () => {
|
|
3110
|
-
runtimeWindow.__timelines = runtimeWindow.__timelines ?? {};
|
|
3111
|
-
runtimeWindow.__timelines[STUDIO_MOTION_TIMELINE_ID]?.kill?.();
|
|
3112
|
-
delete runtimeWindow.__timelines[STUDIO_MOTION_TIMELINE_ID];
|
|
3113
|
-
restoreStudioMotionElements();
|
|
3114
|
-
const gsap = runtimeWindow.gsap;
|
|
3115
|
-
if (!gsap?.timeline || manifestMotions.length === 0) return 0;
|
|
3116
|
-
const timeline = gsap.timeline({ paused: true, defaults: { overwrite: "auto" } });
|
|
3117
|
-
let applied = 0;
|
|
3118
|
-
for (const motionValue of manifestMotions) {
|
|
3119
|
-
const motion = objectRecord(motionValue);
|
|
3120
|
-
if (!motion || motion.kind !== "gsap-motion") continue;
|
|
3121
|
-
const targetRecord = objectRecord(motion.target);
|
|
3122
|
-
if (!targetRecord) continue;
|
|
3123
|
-
const target = resolveTarget(targetRecord);
|
|
3124
|
-
if (!target || typeof timeline.fromTo !== "function") continue;
|
|
3125
|
-
const start = finiteNumber(motion.start);
|
|
3126
|
-
const duration = finiteNumber(motion.duration);
|
|
3127
|
-
if (start == null || duration == null || start < 0 || duration <= 0) continue;
|
|
3128
|
-
const from = parseMotionValues(motion.from);
|
|
3129
|
-
const to = parseMotionValues(motion.to);
|
|
3130
|
-
if (!from || !to) continue;
|
|
3131
|
-
if (!target.hasAttribute(STUDIO_MOTION_ATTR)) {
|
|
3132
|
-
target.setAttribute(ORIGINAL_TRANSFORM_ATTR, target.style.transform);
|
|
3133
|
-
target.setAttribute(ORIGINAL_OPACITY_ATTR, target.style.opacity);
|
|
3134
|
-
target.setAttribute(ORIGINAL_VISIBILITY_ATTR, target.style.visibility);
|
|
3135
|
-
}
|
|
3136
|
-
target.setAttribute(STUDIO_MOTION_ATTR, "true");
|
|
3137
|
-
timeline.fromTo(
|
|
3138
|
-
target,
|
|
3139
|
-
from,
|
|
3140
|
-
{ ...to, duration, ease: resolveEase(motion), overwrite: "auto", immediateRender: false },
|
|
3141
|
-
start
|
|
3142
|
-
);
|
|
3143
|
-
applied += 1;
|
|
3144
|
-
}
|
|
3145
|
-
if (applied === 0) {
|
|
3146
|
-
timeline.kill?.();
|
|
3147
|
-
return 0;
|
|
3148
|
-
}
|
|
3149
|
-
runtimeWindow.__timelines[STUDIO_MOTION_TIMELINE_ID] = timeline;
|
|
3150
|
-
timeline.pause?.();
|
|
3151
|
-
const currentTime = readCurrentTime();
|
|
3152
|
-
if (typeof timeline.totalTime === "function") timeline.totalTime(currentTime, false);
|
|
3153
|
-
else timeline.time?.(currentTime);
|
|
3154
|
-
return applied;
|
|
3155
|
-
};
|
|
3156
|
-
runtimeWindow.__hfStudioMotionApply = applyManifest;
|
|
3157
|
-
applyManifest();
|
|
3158
|
-
}
|
|
3159
|
-
|
|
3160
3029
|
// src/routes/preview.ts
|
|
3161
|
-
import { ensureHfIds as
|
|
3030
|
+
import { ensureHfIds as ensureHfIds2 } from "@hyperframes/parsers/hf-ids";
|
|
3162
3031
|
|
|
3163
3032
|
// src/helpers/hfIdPersist.ts
|
|
3164
|
-
import { ensureHfIds
|
|
3033
|
+
import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
|
|
3165
3034
|
import {
|
|
3166
3035
|
closeSync as closeSync2,
|
|
3167
3036
|
constants,
|
|
3168
3037
|
fstatSync,
|
|
3169
3038
|
ftruncateSync as ftruncateSync2,
|
|
3170
3039
|
openSync as openSync2,
|
|
3171
|
-
readFileSync as
|
|
3040
|
+
readFileSync as readFileSync7,
|
|
3172
3041
|
writeFileSync as writeFileSync5,
|
|
3173
3042
|
writeSync as writeSync2
|
|
3174
3043
|
} from "fs";
|
|
3175
3044
|
function persistHfIdsIfNeeded(filePath, html) {
|
|
3176
|
-
const normalized =
|
|
3045
|
+
const normalized = ensureHfIds(html);
|
|
3177
3046
|
const idsBefore = (html.match(/\bdata-hf-id=/g) ?? []).length;
|
|
3178
3047
|
const idsAfter = (normalized.match(/\bdata-hf-id=/g) ?? []).length;
|
|
3179
3048
|
if (idsAfter > idsBefore) {
|
|
3180
3049
|
try {
|
|
3181
|
-
const current =
|
|
3050
|
+
const current = readFileSync7(filePath, "utf-8");
|
|
3182
3051
|
if (current === html) {
|
|
3183
3052
|
writeFileSync5(filePath, normalized, "utf-8");
|
|
3184
3053
|
}
|
|
@@ -3206,8 +3075,8 @@ function stampFileHfIds(filePath) {
|
|
|
3206
3075
|
if (fd === null) return null;
|
|
3207
3076
|
try {
|
|
3208
3077
|
if (!fstatSync(fd).isFile()) return null;
|
|
3209
|
-
const html =
|
|
3210
|
-
const normalized =
|
|
3078
|
+
const html = readFileSync7(fd, "utf-8");
|
|
3079
|
+
const normalized = ensureHfIds(html);
|
|
3211
3080
|
const idsBefore = (html.match(/\bdata-hf-id=/g) ?? []).length;
|
|
3212
3081
|
const idsAfter = (normalized.match(/\bdata-hf-id=/g) ?? []).length;
|
|
3213
3082
|
if (writable && idsAfter > idsBefore) {
|
|
@@ -3250,9 +3119,9 @@ ${html}`;
|
|
|
3250
3119
|
}
|
|
3251
3120
|
function readStudioMotionManifestContent(projectDir) {
|
|
3252
3121
|
const manifestPath = join8(projectDir, STUDIO_MOTION_PATH);
|
|
3253
|
-
if (!
|
|
3122
|
+
if (!existsSync6(manifestPath)) return "";
|
|
3254
3123
|
try {
|
|
3255
|
-
return
|
|
3124
|
+
return readFileSync8(manifestPath, "utf-8");
|
|
3256
3125
|
} catch {
|
|
3257
3126
|
return "";
|
|
3258
3127
|
}
|
|
@@ -3408,12 +3277,18 @@ async function transformPreviewHtml(html, adapter, project, activeCompositionPat
|
|
|
3408
3277
|
}
|
|
3409
3278
|
function resolveProjectMainHtml(projectDir, projectId) {
|
|
3410
3279
|
const indexPath = join8(projectDir, "index.html");
|
|
3411
|
-
if (
|
|
3412
|
-
return {
|
|
3280
|
+
if (existsSync6(indexPath)) {
|
|
3281
|
+
return {
|
|
3282
|
+
html: readFileSync8(indexPath, "utf-8"),
|
|
3283
|
+
compositionPath: "index.html"
|
|
3284
|
+
};
|
|
3413
3285
|
}
|
|
3414
3286
|
const blockHtmlPath = join8(projectDir, `${projectId}.html`);
|
|
3415
|
-
if (
|
|
3416
|
-
return {
|
|
3287
|
+
if (existsSync6(blockHtmlPath)) {
|
|
3288
|
+
return {
|
|
3289
|
+
html: readFileSync8(blockHtmlPath, "utf-8"),
|
|
3290
|
+
compositionPath: `${projectId}.html`
|
|
3291
|
+
};
|
|
3417
3292
|
}
|
|
3418
3293
|
return null;
|
|
3419
3294
|
}
|
|
@@ -3422,6 +3297,7 @@ function registerPreviewRoutes(api, adapter) {
|
|
|
3422
3297
|
"Cache-Control": "private, no-cache",
|
|
3423
3298
|
ETag: etag
|
|
3424
3299
|
});
|
|
3300
|
+
const mediaCodecProbeCache = resolvePreviewMediaCodecProbeCache(adapter);
|
|
3425
3301
|
api.get("/projects/:id/preview", async (c) => {
|
|
3426
3302
|
const resolved = await resolveProjectAndSignature(adapter, c.req.param("id"));
|
|
3427
3303
|
if (!resolved) return c.json({ error: "not found" }, 404);
|
|
@@ -3432,7 +3308,10 @@ function registerPreviewRoutes(api, adapter) {
|
|
|
3432
3308
|
const etag = `"preview:${signature}${variablesEtagSalt(vars.raw)}"`;
|
|
3433
3309
|
const ifNoneMatch = c.req.header("If-None-Match");
|
|
3434
3310
|
if (ifNoneMatch === etag) {
|
|
3435
|
-
return new Response(null, {
|
|
3311
|
+
return new Response(null, {
|
|
3312
|
+
status: 304,
|
|
3313
|
+
headers: previewCacheHeaders(etag)
|
|
3314
|
+
});
|
|
3436
3315
|
}
|
|
3437
3316
|
const diskMain = resolveProjectMainHtml(project.dir, project.id);
|
|
3438
3317
|
const normalizedDisk = diskMain ? persistHfIdsIfNeeded(join8(project.dir, diskMain.compositionPath), diskMain.html) : null;
|
|
@@ -3455,12 +3334,19 @@ ${runtimeTag}`;
|
|
|
3455
3334
|
bundled = bundled.replace(/<head>/i, `<head><base href="${baseHref}">`);
|
|
3456
3335
|
}
|
|
3457
3336
|
bundled = injectStudioPreviewAugmentations(
|
|
3458
|
-
|
|
3337
|
+
ensureHfIds2(await transformPreviewHtml(bundled, adapter, project, mainCompositionPath)),
|
|
3459
3338
|
adapter,
|
|
3460
3339
|
project.dir,
|
|
3461
3340
|
mainCompositionPath
|
|
3462
3341
|
);
|
|
3463
3342
|
if (previewVariables) bundled = injectPreviewVariables(bundled, previewVariables);
|
|
3343
|
+
bundled = await injectMediaCodecMap(
|
|
3344
|
+
bundled,
|
|
3345
|
+
adapter,
|
|
3346
|
+
project.dir,
|
|
3347
|
+
mainCompositionPath,
|
|
3348
|
+
mediaCodecProbeCache
|
|
3349
|
+
);
|
|
3464
3350
|
return c.html(bundled, 200, previewCacheHeaders(etag));
|
|
3465
3351
|
} catch {
|
|
3466
3352
|
const fallback = resolveProjectMainHtml(project.dir, project.id);
|
|
@@ -3478,6 +3364,13 @@ ${runtimeTag}`;
|
|
|
3478
3364
|
if (previewVariables) {
|
|
3479
3365
|
fallbackAugmented = injectPreviewVariables(fallbackAugmented, previewVariables);
|
|
3480
3366
|
}
|
|
3367
|
+
fallbackAugmented = await injectMediaCodecMap(
|
|
3368
|
+
fallbackAugmented,
|
|
3369
|
+
adapter,
|
|
3370
|
+
project.dir,
|
|
3371
|
+
fallback.compositionPath,
|
|
3372
|
+
mediaCodecProbeCache
|
|
3373
|
+
);
|
|
3481
3374
|
return c.html(fallbackAugmented, 200, previewCacheHeaders(etag));
|
|
3482
3375
|
}
|
|
3483
3376
|
return c.text("not found", 404);
|
|
@@ -3498,13 +3391,16 @@ ${runtimeTag}`;
|
|
|
3498
3391
|
c.req.path.replace(`/projects/${project.id}/preview/comp/`, "").split("?")[0] ?? ""
|
|
3499
3392
|
);
|
|
3500
3393
|
const compFile = resolveWithinProject(project.dir, compPath);
|
|
3501
|
-
if (!compFile || !
|
|
3394
|
+
if (!compFile || !existsSync6(compFile) || !statSync2(compFile).isFile()) {
|
|
3502
3395
|
return c.text("not found", 404);
|
|
3503
3396
|
}
|
|
3504
3397
|
const etag = `"comp:v2:${compPath}:${signature}${variablesEtagSalt(vars.raw)}"`;
|
|
3505
3398
|
const ifNoneMatch = c.req.header("If-None-Match");
|
|
3506
3399
|
if (ifNoneMatch === etag) {
|
|
3507
|
-
return new Response(null, {
|
|
3400
|
+
return new Response(null, {
|
|
3401
|
+
status: 304,
|
|
3402
|
+
headers: previewCacheHeaders(etag)
|
|
3403
|
+
});
|
|
3508
3404
|
}
|
|
3509
3405
|
const stamped = pinSubCompHfIds(compFile, compPath);
|
|
3510
3406
|
if (stamped === null) return c.text("not found", 404);
|
|
@@ -3517,9 +3413,10 @@ ${runtimeTag}`;
|
|
|
3517
3413
|
stamped
|
|
3518
3414
|
);
|
|
3519
3415
|
if (!html) return c.text("not found", 404);
|
|
3520
|
-
html =
|
|
3416
|
+
html = ensureHfIds2(await transformPreviewHtml(html, adapter, project, compPath));
|
|
3521
3417
|
html = injectStudioPreviewAugmentations(html, adapter, project.dir, compPath);
|
|
3522
3418
|
if (previewVariables) html = injectPreviewVariables(html, previewVariables);
|
|
3419
|
+
html = await injectMediaCodecMap(html, adapter, project.dir, compPath, mediaCodecProbeCache);
|
|
3523
3420
|
return c.html(html, 200, previewCacheHeaders(etag));
|
|
3524
3421
|
});
|
|
3525
3422
|
api.get("/projects/:id/preview/*", async (c) => {
|
|
@@ -3532,21 +3429,55 @@ ${runtimeTag}`;
|
|
|
3532
3429
|
if (!file) {
|
|
3533
3430
|
return c.text("not found", 404);
|
|
3534
3431
|
}
|
|
3535
|
-
const stat =
|
|
3432
|
+
const stat = existsSync6(file) ? statSync2(file) : null;
|
|
3536
3433
|
if (!stat?.isFile()) {
|
|
3537
3434
|
return c.text("not found", 404);
|
|
3538
3435
|
}
|
|
3539
3436
|
const contentType = getMimeType(subPath);
|
|
3540
3437
|
const isText = /\.(html|css|js|json|svg|txt|md|cube)$/i.test(subPath);
|
|
3541
|
-
const
|
|
3542
|
-
|
|
3438
|
+
const proxyParam = c.req.query("hf-proxy");
|
|
3439
|
+
let proxyVariant;
|
|
3440
|
+
if (proxyParam !== void 0) {
|
|
3441
|
+
if (!isProxyVariantRequest(proxyParam) || !contentType.startsWith("video/") || !isAutoProxyEnabled(adapter)) {
|
|
3442
|
+
return c.text("not found", 404);
|
|
3443
|
+
}
|
|
3444
|
+
const facts = await probeAssetCodec(file);
|
|
3445
|
+
const eligibility = decideMediaProxyEligibility(facts);
|
|
3446
|
+
if (!eligibility.eligible) {
|
|
3447
|
+
return c.text(`media proxy unavailable: ${eligibility.reason}`, 422);
|
|
3448
|
+
}
|
|
3449
|
+
if (!facts) return c.text("media proxy unavailable: unknown_codec", 422);
|
|
3450
|
+
proxyVariant = resolveProxyVariantRequest(proxyParam, facts) ?? void 0;
|
|
3451
|
+
if (!proxyVariant) {
|
|
3452
|
+
return c.text("media proxy variant does not match asset", 422);
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
const etag = `"${stat.mtimeMs.toString(36)}-${stat.size.toString(36)}${proxyEtagSalt(proxyVariant)}"`;
|
|
3456
|
+
const cacheHeaders = isText ? { "Cache-Control": "no-store" } : {
|
|
3457
|
+
"Cache-Control": "private, max-age=3600, must-revalidate",
|
|
3458
|
+
ETag: etag
|
|
3459
|
+
};
|
|
3543
3460
|
if (!isText) {
|
|
3544
3461
|
const ifNoneMatch = c.req.header("If-None-Match");
|
|
3545
3462
|
if (ifNoneMatch === etag) {
|
|
3546
3463
|
return new Response(null, { status: 304, headers: cacheHeaders });
|
|
3547
3464
|
}
|
|
3548
3465
|
}
|
|
3549
|
-
|
|
3466
|
+
let servedPath = file;
|
|
3467
|
+
let servedContentType = contentType;
|
|
3468
|
+
if (proxyVariant !== void 0) {
|
|
3469
|
+
try {
|
|
3470
|
+
servedPath = await resolveProxy(project.dir, file, proxyVariant);
|
|
3471
|
+
} catch (err) {
|
|
3472
|
+
if (err instanceof ProxyCapacityError) {
|
|
3473
|
+
return c.text(err.message, 503, { "Retry-After": "5" });
|
|
3474
|
+
}
|
|
3475
|
+
const message = err instanceof ProxyTranscodeError ? err.message : "proxy transcode failed";
|
|
3476
|
+
return c.text(message, 502);
|
|
3477
|
+
}
|
|
3478
|
+
servedContentType = PROXY_VARIANT_CONFIG[proxyVariant].contentType;
|
|
3479
|
+
}
|
|
3480
|
+
const buffer = isText ? Buffer.from(readFileSync8(file, "utf-8"), "utf-8") : readFileSync8(servedPath);
|
|
3550
3481
|
const totalSize = buffer.length;
|
|
3551
3482
|
const rangeHeader = c.req.header("Range");
|
|
3552
3483
|
if (rangeHeader) {
|
|
@@ -3560,7 +3491,7 @@ ${runtimeTag}`;
|
|
|
3560
3491
|
status: 206,
|
|
3561
3492
|
headers: {
|
|
3562
3493
|
...cacheHeaders,
|
|
3563
|
-
"Content-Type":
|
|
3494
|
+
"Content-Type": servedContentType,
|
|
3564
3495
|
"Content-Range": `bytes ${start}-${safeEnd}/${totalSize}`,
|
|
3565
3496
|
"Accept-Ranges": "bytes",
|
|
3566
3497
|
"Content-Length": String(chunkSize)
|
|
@@ -3571,7 +3502,7 @@ ${runtimeTag}`;
|
|
|
3571
3502
|
return new Response(new Uint8Array(buffer), {
|
|
3572
3503
|
headers: {
|
|
3573
3504
|
...cacheHeaders,
|
|
3574
|
-
"Content-Type":
|
|
3505
|
+
"Content-Type": servedContentType,
|
|
3575
3506
|
"Accept-Ranges": "bytes",
|
|
3576
3507
|
"Content-Length": String(totalSize)
|
|
3577
3508
|
}
|
|
@@ -3580,7 +3511,7 @@ ${runtimeTag}`;
|
|
|
3580
3511
|
}
|
|
3581
3512
|
|
|
3582
3513
|
// src/routes/lint.ts
|
|
3583
|
-
import { readFileSync as
|
|
3514
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
3584
3515
|
import { join as join9 } from "path";
|
|
3585
3516
|
function registerLintRoutes(api, adapter) {
|
|
3586
3517
|
api.get("/projects/:id/lint", async (c) => {
|
|
@@ -3592,7 +3523,7 @@ function registerLintRoutes(api, adapter) {
|
|
|
3592
3523
|
);
|
|
3593
3524
|
const allFindings = [];
|
|
3594
3525
|
for (const file of htmlFiles) {
|
|
3595
|
-
const content =
|
|
3526
|
+
const content = readFileSync9(join9(project.dir, file), "utf-8");
|
|
3596
3527
|
const result = await adapter.lint(content, { filePath: file });
|
|
3597
3528
|
if (result?.findings) {
|
|
3598
3529
|
for (const f of result.findings) {
|
|
@@ -3610,7 +3541,7 @@ function registerLintRoutes(api, adapter) {
|
|
|
3610
3541
|
|
|
3611
3542
|
// src/routes/render.ts
|
|
3612
3543
|
import { streamSSE } from "hono/streaming";
|
|
3613
|
-
import { existsSync as
|
|
3544
|
+
import { existsSync as existsSync7, readFileSync as readFileSync10, mkdirSync as mkdirSync4, unlinkSync as unlinkSync3, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
|
|
3614
3545
|
import { join as join10 } from "path";
|
|
3615
3546
|
import { VALID_CANVAS_RESOLUTIONS } from "@hyperframes/parsers";
|
|
3616
3547
|
import { formatRenderOutputTimestamp, parseFps } from "@hyperframes/core";
|
|
@@ -3669,7 +3600,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
3669
3600
|
const now = /* @__PURE__ */ new Date();
|
|
3670
3601
|
const jobId = `${project.id}_${formatRenderOutputTimestamp(now)}`;
|
|
3671
3602
|
const rendersDir = adapter.rendersDir(project);
|
|
3672
|
-
if (!
|
|
3603
|
+
if (!existsSync7(rendersDir)) mkdirSync4(rendersDir, { recursive: true });
|
|
3673
3604
|
const ext = FORMAT_EXT[format] ?? ".mp4";
|
|
3674
3605
|
const outputPath = join10(rendersDir, `${jobId}${ext}`);
|
|
3675
3606
|
const jobState = adapter.startRender({
|
|
@@ -3734,12 +3665,12 @@ function registerRenderRoutes(api, adapter) {
|
|
|
3734
3665
|
api.get("/render/:jobId/view", (c) => {
|
|
3735
3666
|
const { jobId } = c.req.param();
|
|
3736
3667
|
const job = renderJobs.get(jobId);
|
|
3737
|
-
if (!job?.outputPath || !
|
|
3668
|
+
if (!job?.outputPath || !existsSync7(job.outputPath)) {
|
|
3738
3669
|
return c.json({ error: "not found" }, 404);
|
|
3739
3670
|
}
|
|
3740
3671
|
const contentType = renderContentType(job.outputPath);
|
|
3741
3672
|
const filename = job.outputPath.split("/").pop() ?? `render.mp4`;
|
|
3742
|
-
const content =
|
|
3673
|
+
const content = readFileSync10(job.outputPath);
|
|
3743
3674
|
return new Response(content, {
|
|
3744
3675
|
headers: {
|
|
3745
3676
|
"Content-Type": contentType,
|
|
@@ -3752,12 +3683,12 @@ function registerRenderRoutes(api, adapter) {
|
|
|
3752
3683
|
api.get("/render/:jobId/download", (c) => {
|
|
3753
3684
|
const { jobId } = c.req.param();
|
|
3754
3685
|
const job = renderJobs.get(jobId);
|
|
3755
|
-
if (!job?.outputPath || !
|
|
3686
|
+
if (!job?.outputPath || !existsSync7(job.outputPath)) {
|
|
3756
3687
|
return c.json({ error: "not found" }, 404);
|
|
3757
3688
|
}
|
|
3758
3689
|
const contentType = renderContentType(job.outputPath);
|
|
3759
3690
|
const filename = job.outputPath.split("/").pop() ?? `render.mp4`;
|
|
3760
|
-
const content =
|
|
3691
|
+
const content = readFileSync10(job.outputPath);
|
|
3761
3692
|
return new Response(content, {
|
|
3762
3693
|
headers: {
|
|
3763
3694
|
"Content-Type": contentType,
|
|
@@ -3772,7 +3703,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
3772
3703
|
const dir = state.outputPath.replace(/\/[^/]+$/, "");
|
|
3773
3704
|
for (const ext of [".mp4", ".webm", ".mov", ".meta.json"]) {
|
|
3774
3705
|
const fp = join10(dir, `${jobId}${ext}`);
|
|
3775
|
-
if (
|
|
3706
|
+
if (existsSync7(fp)) unlinkSync3(fp);
|
|
3776
3707
|
}
|
|
3777
3708
|
break;
|
|
3778
3709
|
}
|
|
@@ -3788,9 +3719,9 @@ function registerRenderRoutes(api, adapter) {
|
|
|
3788
3719
|
const rendersDir = adapter.rendersDir(project);
|
|
3789
3720
|
const fp = resolveWithinProject(rendersDir, filename);
|
|
3790
3721
|
if (!fp) return c.json({ error: "forbidden" }, 403);
|
|
3791
|
-
if (!
|
|
3722
|
+
if (!existsSync7(fp)) return c.json({ error: "not found" }, 404);
|
|
3792
3723
|
const contentType = renderContentType(fp);
|
|
3793
|
-
const content =
|
|
3724
|
+
const content = readFileSync10(fp);
|
|
3794
3725
|
return new Response(content, {
|
|
3795
3726
|
headers: {
|
|
3796
3727
|
"Content-Type": contentType,
|
|
@@ -3804,7 +3735,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
3804
3735
|
const project = await adapter.resolveProject(c.req.param("id"));
|
|
3805
3736
|
if (!project) return c.json({ error: "not found" }, 404);
|
|
3806
3737
|
const rendersDir = adapter.rendersDir(project);
|
|
3807
|
-
if (!
|
|
3738
|
+
if (!existsSync7(rendersDir)) return c.json({ renders: [] });
|
|
3808
3739
|
const files = readdirSync5(rendersDir).filter((f) => f.endsWith(".mp4") || f.endsWith(".webm") || f.endsWith(".mov")).map((f) => {
|
|
3809
3740
|
const fp = join10(rendersDir, f);
|
|
3810
3741
|
const stat = statSync3(fp);
|
|
@@ -3812,10 +3743,10 @@ function registerRenderRoutes(api, adapter) {
|
|
|
3812
3743
|
const metaPath = join10(rendersDir, `${rid}.meta.json`);
|
|
3813
3744
|
let status = "complete";
|
|
3814
3745
|
let durationMs;
|
|
3815
|
-
if (
|
|
3746
|
+
if (existsSync7(metaPath)) {
|
|
3816
3747
|
try {
|
|
3817
|
-
const meta = JSON.parse(
|
|
3818
|
-
if (meta.status === "failed") status = "failed";
|
|
3748
|
+
const meta = JSON.parse(readFileSync10(metaPath, "utf-8"));
|
|
3749
|
+
if (meta.status === "failed" && !existsSync7(fp)) status = "failed";
|
|
3819
3750
|
if (meta.durationMs) durationMs = meta.durationMs;
|
|
3820
3751
|
} catch {
|
|
3821
3752
|
}
|
|
@@ -3845,589 +3776,9 @@ function registerRenderRoutes(api, adapter) {
|
|
|
3845
3776
|
}
|
|
3846
3777
|
|
|
3847
3778
|
// src/routes/thumbnail.ts
|
|
3848
|
-
import { existsSync as
|
|
3779
|
+
import { existsSync as existsSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, statSync as statSync4 } from "fs";
|
|
3849
3780
|
import { join as join11 } from "path";
|
|
3850
3781
|
import { createHash as createHash4 } from "crypto";
|
|
3851
|
-
|
|
3852
|
-
// src/helpers/manualEditsRenderScript.ts
|
|
3853
|
-
var STUDIO_MANUAL_EDITS_PATH = ".hyperframes/studio-manual-edits.json";
|
|
3854
|
-
function createStudioManualEditsRenderBodyScript(manifestContent, options = {}) {
|
|
3855
|
-
if (!manifestContent.trim()) return null;
|
|
3856
|
-
return `(${studioManualEditsRenderRuntime.toString()})(${JSON.stringify(manifestContent)}, ${JSON.stringify(options.activeCompositionPath ?? null)});`;
|
|
3857
|
-
}
|
|
3858
|
-
function createStudioPositionSeekReapplyScript() {
|
|
3859
|
-
return `(${studioPositionSeekReapplyRuntime.toString()})();`;
|
|
3860
|
-
}
|
|
3861
|
-
function studioPositionSeekReapplyRuntime() {
|
|
3862
|
-
const OFFSET_X_PROP = "--hf-studio-offset-x";
|
|
3863
|
-
const OFFSET_Y_PROP = "--hf-studio-offset-y";
|
|
3864
|
-
const WIDTH_PROP = "--hf-studio-width";
|
|
3865
|
-
const HEIGHT_PROP = "--hf-studio-height";
|
|
3866
|
-
const ROTATION_PROP = "--hf-studio-rotation";
|
|
3867
|
-
const PATH_OFFSET_ATTR = "data-hf-studio-path-offset";
|
|
3868
|
-
const BOX_SIZE_ATTR = "data-hf-studio-box-size";
|
|
3869
|
-
const ROTATION_ATTR = "data-hf-studio-rotation";
|
|
3870
|
-
const ORIGINAL_TRANSLATE_ATTR = "data-hf-studio-original-translate";
|
|
3871
|
-
const ORIGINAL_ROTATE_ATTR = "data-hf-studio-original-rotate";
|
|
3872
|
-
const MOTION_ATTR = "data-hf-studio-motion";
|
|
3873
|
-
const MOTION_TL_KEY = "studio-motion";
|
|
3874
|
-
const WRAPPED_PROP = "__hfStudioPositionSeekReapplyWrapped";
|
|
3875
|
-
if (!document.querySelector("[" + PATH_OFFSET_ATTR + '="true"]') && !document.querySelector("[" + BOX_SIZE_ATTR + '="true"]') && !document.querySelector("[" + ROTATION_ATTR + '="true"]') && !document.querySelector("[" + MOTION_ATTR + "]"))
|
|
3876
|
-
return;
|
|
3877
|
-
const splitTopLevelWhitespace = (value) => {
|
|
3878
|
-
const parts = [];
|
|
3879
|
-
let depth = 0;
|
|
3880
|
-
let current = "";
|
|
3881
|
-
for (const char of value.trim()) {
|
|
3882
|
-
if (char === "(") depth += 1;
|
|
3883
|
-
if (char === ")") depth = Math.max(0, depth - 1);
|
|
3884
|
-
if (/\s/.test(char) && depth === 0) {
|
|
3885
|
-
if (current) parts.push(current);
|
|
3886
|
-
current = "";
|
|
3887
|
-
} else {
|
|
3888
|
-
current += char;
|
|
3889
|
-
}
|
|
3890
|
-
}
|
|
3891
|
-
if (current) parts.push(current);
|
|
3892
|
-
return parts;
|
|
3893
|
-
};
|
|
3894
|
-
const composeTranslate = (element, x, y) => {
|
|
3895
|
-
const original = element.getAttribute(ORIGINAL_TRANSLATE_ATTR)?.trim();
|
|
3896
|
-
if (!original || original === "none") return x + " " + y;
|
|
3897
|
-
const parts = splitTopLevelWhitespace(original);
|
|
3898
|
-
if (parts.length === 1) return "calc(" + parts[0] + " + " + x + ") " + y;
|
|
3899
|
-
if (parts.length >= 2) {
|
|
3900
|
-
const z = parts.length >= 3 ? " " + parts[2] : "";
|
|
3901
|
-
return "calc(" + parts[0] + " + " + x + ") calc(" + parts[1] + " + " + y + ")" + z;
|
|
3902
|
-
}
|
|
3903
|
-
return x + " " + y;
|
|
3904
|
-
};
|
|
3905
|
-
const isSimpleRotateAngle = (value) => /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:deg|rad|turn|grad)$/.test(value.trim());
|
|
3906
|
-
const composeRotation = (element, rotationValue) => {
|
|
3907
|
-
const original = element.getAttribute(ORIGINAL_ROTATE_ATTR)?.trim();
|
|
3908
|
-
if (!original || original === "none" || !isSimpleRotateAngle(original)) return rotationValue;
|
|
3909
|
-
return "calc(" + original + " + " + rotationValue + ")";
|
|
3910
|
-
};
|
|
3911
|
-
let lastSeekTime = 0;
|
|
3912
|
-
let cachedMotionKey = "";
|
|
3913
|
-
const finiteNum = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
3914
|
-
const computeMotionKey = (motionEls) => {
|
|
3915
|
-
let key = "";
|
|
3916
|
-
for (let i = 0; i < motionEls.length; i++) {
|
|
3917
|
-
const json = motionEls[i].getAttribute?.(MOTION_ATTR);
|
|
3918
|
-
if (json) key += (key ? "\n" : "") + json;
|
|
3919
|
-
}
|
|
3920
|
-
return key;
|
|
3921
|
-
};
|
|
3922
|
-
const reapplyMotionTimeline = () => {
|
|
3923
|
-
const motionEls = document.querySelectorAll("[" + MOTION_ATTR + "]");
|
|
3924
|
-
if (motionEls.length === 0) {
|
|
3925
|
-
cachedMotionKey = "";
|
|
3926
|
-
return;
|
|
3927
|
-
}
|
|
3928
|
-
const win = window;
|
|
3929
|
-
const gsap = win.gsap;
|
|
3930
|
-
if (!gsap || typeof gsap.timeline !== "function") return;
|
|
3931
|
-
win.__timelines = win.__timelines || {};
|
|
3932
|
-
const motionKey = computeMotionKey(motionEls);
|
|
3933
|
-
const existing = win.__timelines[MOTION_TL_KEY];
|
|
3934
|
-
if (motionKey && motionKey === cachedMotionKey && existing && typeof existing.totalTime === "function") {
|
|
3935
|
-
existing.totalTime(lastSeekTime, false);
|
|
3936
|
-
return;
|
|
3937
|
-
}
|
|
3938
|
-
if (existing && typeof existing.kill === "function") existing.kill();
|
|
3939
|
-
const tl = gsap.timeline({ paused: true, defaults: { overwrite: "auto" } });
|
|
3940
|
-
const fromTo = tl.fromTo;
|
|
3941
|
-
if (typeof fromTo !== "function") return;
|
|
3942
|
-
let applied = 0;
|
|
3943
|
-
for (let i = 0; i < motionEls.length; i++) {
|
|
3944
|
-
const el = motionEls[i];
|
|
3945
|
-
if (!(el instanceof HTMLElement)) continue;
|
|
3946
|
-
const json = el.getAttribute(MOTION_ATTR);
|
|
3947
|
-
if (!json) continue;
|
|
3948
|
-
try {
|
|
3949
|
-
const m = JSON.parse(json);
|
|
3950
|
-
const start = finiteNum(m.start);
|
|
3951
|
-
const duration = finiteNum(m.duration);
|
|
3952
|
-
if (start == null || duration == null || duration <= 0) continue;
|
|
3953
|
-
const ease = typeof m.ease === "string" ? m.ease : "none";
|
|
3954
|
-
const from = m.from && typeof m.from === "object" ? m.from : {};
|
|
3955
|
-
const to = m.to && typeof m.to === "object" ? m.to : {};
|
|
3956
|
-
const customEase = m.customEase;
|
|
3957
|
-
let resolvedEase = ease;
|
|
3958
|
-
if (customEase?.id && customEase?.data && win.CustomEase?.create) {
|
|
3959
|
-
try {
|
|
3960
|
-
gsap.registerPlugin?.(win.CustomEase);
|
|
3961
|
-
win.CustomEase.create(customEase.id, customEase.data);
|
|
3962
|
-
resolvedEase = customEase.id;
|
|
3963
|
-
} catch {
|
|
3964
|
-
}
|
|
3965
|
-
}
|
|
3966
|
-
fromTo.call(
|
|
3967
|
-
tl,
|
|
3968
|
-
el,
|
|
3969
|
-
{ ...from },
|
|
3970
|
-
{ ...to, duration, ease: resolvedEase, overwrite: "auto", immediateRender: false },
|
|
3971
|
-
start
|
|
3972
|
-
);
|
|
3973
|
-
applied += 1;
|
|
3974
|
-
} catch {
|
|
3975
|
-
}
|
|
3976
|
-
}
|
|
3977
|
-
if (applied === 0) {
|
|
3978
|
-
cachedMotionKey = "";
|
|
3979
|
-
if (typeof tl.kill === "function")
|
|
3980
|
-
tl.kill();
|
|
3981
|
-
return;
|
|
3982
|
-
}
|
|
3983
|
-
cachedMotionKey = motionKey;
|
|
3984
|
-
win.__timelines[MOTION_TL_KEY] = tl;
|
|
3985
|
-
if (typeof tl.pause === "function") tl.pause();
|
|
3986
|
-
if (typeof tl.totalTime === "function")
|
|
3987
|
-
tl.totalTime(lastSeekTime, false);
|
|
3988
|
-
};
|
|
3989
|
-
const stripGsapTranslateFromTransform = (el) => {
|
|
3990
|
-
const transform = el.style.getPropertyValue("transform");
|
|
3991
|
-
if (!transform || transform === "none") return;
|
|
3992
|
-
const win = el.ownerDocument.defaultView;
|
|
3993
|
-
const MatrixCtor = win?.DOMMatrix;
|
|
3994
|
-
if (!MatrixCtor) return;
|
|
3995
|
-
try {
|
|
3996
|
-
const m = new MatrixCtor(transform);
|
|
3997
|
-
if (m.m41 === 0 && m.m42 === 0) return;
|
|
3998
|
-
m.m41 = 0;
|
|
3999
|
-
m.m42 = 0;
|
|
4000
|
-
if (m.is2D && m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1) {
|
|
4001
|
-
el.style.removeProperty("transform");
|
|
4002
|
-
} else {
|
|
4003
|
-
el.style.setProperty("transform", m.toString());
|
|
4004
|
-
}
|
|
4005
|
-
} catch {
|
|
4006
|
-
}
|
|
4007
|
-
};
|
|
4008
|
-
const reapplyAll = () => {
|
|
4009
|
-
const offsetEls = document.querySelectorAll("[" + PATH_OFFSET_ATTR + '="true"]');
|
|
4010
|
-
for (let i = 0; i < offsetEls.length; i++) {
|
|
4011
|
-
const el = offsetEls[i];
|
|
4012
|
-
if (!(el instanceof HTMLElement)) continue;
|
|
4013
|
-
const x = el.style.getPropertyValue(OFFSET_X_PROP);
|
|
4014
|
-
const y = el.style.getPropertyValue(OFFSET_Y_PROP);
|
|
4015
|
-
if (x || y) {
|
|
4016
|
-
el.style.setProperty(
|
|
4017
|
-
"translate",
|
|
4018
|
-
composeTranslate(
|
|
4019
|
-
el,
|
|
4020
|
-
"var(" + OFFSET_X_PROP + ", 0px)",
|
|
4021
|
-
"var(" + OFFSET_Y_PROP + ", 0px)"
|
|
4022
|
-
)
|
|
4023
|
-
);
|
|
4024
|
-
stripGsapTranslateFromTransform(el);
|
|
4025
|
-
}
|
|
4026
|
-
}
|
|
4027
|
-
const boxSizeEls = document.querySelectorAll("[" + BOX_SIZE_ATTR + '="true"]');
|
|
4028
|
-
for (let i = 0; i < boxSizeEls.length; i++) {
|
|
4029
|
-
const el = boxSizeEls[i];
|
|
4030
|
-
if (!(el instanceof HTMLElement)) continue;
|
|
4031
|
-
const w = el.style.getPropertyValue(WIDTH_PROP);
|
|
4032
|
-
const h = el.style.getPropertyValue(HEIGHT_PROP);
|
|
4033
|
-
if (w) el.style.setProperty("width", w);
|
|
4034
|
-
if (h) el.style.setProperty("height", h);
|
|
4035
|
-
}
|
|
4036
|
-
const rotEls = document.querySelectorAll("[" + ROTATION_ATTR + '="true"]');
|
|
4037
|
-
for (let i = 0; i < rotEls.length; i++) {
|
|
4038
|
-
const el = rotEls[i];
|
|
4039
|
-
if (!(el instanceof HTMLElement)) continue;
|
|
4040
|
-
const rot = el.style.getPropertyValue(ROTATION_PROP);
|
|
4041
|
-
if (rot) {
|
|
4042
|
-
el.style.setProperty("rotate", composeRotation(el, "var(" + ROTATION_PROP + ", 0deg)"));
|
|
4043
|
-
stripGsapTranslateFromTransform(el);
|
|
4044
|
-
}
|
|
4045
|
-
}
|
|
4046
|
-
reapplyMotionTimeline();
|
|
4047
|
-
};
|
|
4048
|
-
const runtimeWindow = window;
|
|
4049
|
-
const isWrapped = (fn) => Boolean(fn[WRAPPED_PROP]);
|
|
4050
|
-
const markWrapped = (fn) => {
|
|
4051
|
-
try {
|
|
4052
|
-
Object.defineProperty(fn, WRAPPED_PROP, {
|
|
4053
|
-
configurable: false,
|
|
4054
|
-
enumerable: false,
|
|
4055
|
-
value: true
|
|
4056
|
-
});
|
|
4057
|
-
} catch {
|
|
4058
|
-
try {
|
|
4059
|
-
fn[WRAPPED_PROP] = true;
|
|
4060
|
-
} catch {
|
|
4061
|
-
}
|
|
4062
|
-
}
|
|
4063
|
-
};
|
|
4064
|
-
const wrapFn = (get, set) => {
|
|
4065
|
-
const fn = get();
|
|
4066
|
-
if (typeof fn !== "function") return false;
|
|
4067
|
-
const seek = fn;
|
|
4068
|
-
if (isWrapped(seek)) {
|
|
4069
|
-
reapplyAll();
|
|
4070
|
-
return true;
|
|
4071
|
-
}
|
|
4072
|
-
const wrapped = function(time) {
|
|
4073
|
-
lastSeekTime = typeof time === "number" && Number.isFinite(time) ? Math.max(0, time) : 0;
|
|
4074
|
-
const result = seek.call(this, time);
|
|
4075
|
-
reapplyAll();
|
|
4076
|
-
return result;
|
|
4077
|
-
};
|
|
4078
|
-
markWrapped(wrapped);
|
|
4079
|
-
set(wrapped);
|
|
4080
|
-
reapplyAll();
|
|
4081
|
-
return true;
|
|
4082
|
-
};
|
|
4083
|
-
const wrapSeekFunctions = () => {
|
|
4084
|
-
const a = wrapFn(
|
|
4085
|
-
() => runtimeWindow.__hf?.["seek"],
|
|
4086
|
-
(fn) => {
|
|
4087
|
-
if (runtimeWindow.__hf) runtimeWindow.__hf["seek"] = fn;
|
|
4088
|
-
}
|
|
4089
|
-
);
|
|
4090
|
-
const b = wrapFn(
|
|
4091
|
-
() => runtimeWindow.__player?.["renderSeek"],
|
|
4092
|
-
(fn) => {
|
|
4093
|
-
if (runtimeWindow.__player) runtimeWindow.__player["renderSeek"] = fn;
|
|
4094
|
-
}
|
|
4095
|
-
);
|
|
4096
|
-
return a || b;
|
|
4097
|
-
};
|
|
4098
|
-
const installSeekTrap = (obj, key, getter, setter) => {
|
|
4099
|
-
if (!obj) return;
|
|
4100
|
-
try {
|
|
4101
|
-
let current = obj[key];
|
|
4102
|
-
Object.defineProperty(obj, key, {
|
|
4103
|
-
configurable: true,
|
|
4104
|
-
enumerable: true,
|
|
4105
|
-
get() {
|
|
4106
|
-
return current;
|
|
4107
|
-
},
|
|
4108
|
-
set(value) {
|
|
4109
|
-
current = value;
|
|
4110
|
-
if (typeof value === "function" && !isWrapped(value)) {
|
|
4111
|
-
wrapFn(getter, setter);
|
|
4112
|
-
}
|
|
4113
|
-
}
|
|
4114
|
-
});
|
|
4115
|
-
} catch {
|
|
4116
|
-
}
|
|
4117
|
-
};
|
|
4118
|
-
if (document.readyState === "loading") {
|
|
4119
|
-
document.addEventListener("DOMContentLoaded", () => reapplyAll(), { once: true });
|
|
4120
|
-
} else {
|
|
4121
|
-
reapplyAll();
|
|
4122
|
-
}
|
|
4123
|
-
wrapSeekFunctions();
|
|
4124
|
-
installSeekTrap(
|
|
4125
|
-
runtimeWindow.__hf,
|
|
4126
|
-
"seek",
|
|
4127
|
-
() => runtimeWindow.__hf?.["seek"],
|
|
4128
|
-
(fn) => {
|
|
4129
|
-
if (runtimeWindow.__hf) runtimeWindow.__hf["seek"] = fn;
|
|
4130
|
-
}
|
|
4131
|
-
);
|
|
4132
|
-
installSeekTrap(
|
|
4133
|
-
runtimeWindow.__player,
|
|
4134
|
-
"renderSeek",
|
|
4135
|
-
() => runtimeWindow.__player?.["renderSeek"],
|
|
4136
|
-
(fn) => {
|
|
4137
|
-
if (runtimeWindow.__player) runtimeWindow.__player["renderSeek"] = fn;
|
|
4138
|
-
}
|
|
4139
|
-
);
|
|
4140
|
-
let remaining = 120;
|
|
4141
|
-
const interval = setInterval(() => {
|
|
4142
|
-
wrapSeekFunctions();
|
|
4143
|
-
remaining -= 1;
|
|
4144
|
-
if (remaining <= 0) clearInterval(interval);
|
|
4145
|
-
}, 50);
|
|
4146
|
-
}
|
|
4147
|
-
function studioManualEditsRenderRuntime(manifestContent, activeCompositionPath) {
|
|
4148
|
-
const OFFSET_X_PROP = "--hf-studio-offset-x";
|
|
4149
|
-
const OFFSET_Y_PROP = "--hf-studio-offset-y";
|
|
4150
|
-
const WIDTH_PROP = "--hf-studio-width";
|
|
4151
|
-
const HEIGHT_PROP = "--hf-studio-height";
|
|
4152
|
-
const ROTATION_PROP = "--hf-studio-rotation";
|
|
4153
|
-
const PATH_OFFSET_ATTR = "data-hf-studio-path-offset";
|
|
4154
|
-
const BOX_SIZE_ATTR = "data-hf-studio-box-size";
|
|
4155
|
-
const ROTATION_ATTR = "data-hf-studio-rotation";
|
|
4156
|
-
const ORIGINAL_TRANSLATE_ATTR = "data-hf-studio-original-translate";
|
|
4157
|
-
const ORIGINAL_ROTATE_ATTR = "data-hf-studio-original-rotate";
|
|
4158
|
-
const WRAPPED_SEEK_PROP = "__hfStudioManualEditsWrapped";
|
|
4159
|
-
const ROTATION_TRANSFORM_ORIGIN = "center center";
|
|
4160
|
-
const finiteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
4161
|
-
const objectRecord = (value) => value && typeof value === "object" ? value : null;
|
|
4162
|
-
const runtimeWindow = window;
|
|
4163
|
-
const parsedManifest = (() => {
|
|
4164
|
-
try {
|
|
4165
|
-
return objectRecord(JSON.parse(manifestContent));
|
|
4166
|
-
} catch {
|
|
4167
|
-
return null;
|
|
4168
|
-
}
|
|
4169
|
-
})();
|
|
4170
|
-
const manifestEdits = Array.isArray(parsedManifest?.edits) ? parsedManifest.edits : [];
|
|
4171
|
-
if (manifestEdits.length === 0) return;
|
|
4172
|
-
const sourceFileForElement = (element) => {
|
|
4173
|
-
let current = element;
|
|
4174
|
-
while (current) {
|
|
4175
|
-
const sourceFile = current.getAttribute("data-composition-file") ?? current.getAttribute("data-composition-src");
|
|
4176
|
-
if (sourceFile) return sourceFile;
|
|
4177
|
-
current = current.parentElement;
|
|
4178
|
-
}
|
|
4179
|
-
return activeCompositionPath ?? "index.html";
|
|
4180
|
-
};
|
|
4181
|
-
const elementMatchesSourceFile = (element, sourceFile) => sourceFileForElement(element) === sourceFile;
|
|
4182
|
-
const styleUsesStudioOffset = (value) => value.includes(OFFSET_X_PROP) || value.includes(OFFSET_Y_PROP);
|
|
4183
|
-
const styleUsesStudioRotation = (value) => value.includes(ROTATION_PROP);
|
|
4184
|
-
const splitTopLevelWhitespace = (value) => {
|
|
4185
|
-
const parts = [];
|
|
4186
|
-
let depth = 0;
|
|
4187
|
-
let current = "";
|
|
4188
|
-
for (const char of value.trim()) {
|
|
4189
|
-
if (char === "(") depth += 1;
|
|
4190
|
-
if (char === ")") depth = Math.max(0, depth - 1);
|
|
4191
|
-
if (/\s/.test(char) && depth === 0) {
|
|
4192
|
-
if (current) parts.push(current);
|
|
4193
|
-
current = "";
|
|
4194
|
-
} else {
|
|
4195
|
-
current += char;
|
|
4196
|
-
}
|
|
4197
|
-
}
|
|
4198
|
-
if (current) parts.push(current);
|
|
4199
|
-
return parts;
|
|
4200
|
-
};
|
|
4201
|
-
const composeTranslate = (element, x, y) => {
|
|
4202
|
-
const original = element.getAttribute(ORIGINAL_TRANSLATE_ATTR)?.trim();
|
|
4203
|
-
if (!original || original === "none") return `${x} ${y}`;
|
|
4204
|
-
const parts = splitTopLevelWhitespace(original);
|
|
4205
|
-
if (parts.length === 1) return `calc(${parts[0]} + ${x}) ${y}`;
|
|
4206
|
-
if (parts.length === 2) return `calc(${parts[0]} + ${x}) calc(${parts[1]} + ${y})`;
|
|
4207
|
-
if (parts.length === 3) {
|
|
4208
|
-
return `calc(${parts[0]} + ${x}) calc(${parts[1]} + ${y}) ${parts[2]}`;
|
|
4209
|
-
}
|
|
4210
|
-
return `${x} ${y}`;
|
|
4211
|
-
};
|
|
4212
|
-
const readStyleOrComputed = (element, property) => {
|
|
4213
|
-
try {
|
|
4214
|
-
return element.style.getPropertyValue(property) || getComputedStyle(element).getPropertyValue(property);
|
|
4215
|
-
} catch {
|
|
4216
|
-
return element.style.getPropertyValue(property);
|
|
4217
|
-
}
|
|
4218
|
-
};
|
|
4219
|
-
const readTransformLonghandBase = (element, property) => {
|
|
4220
|
-
const value = readStyleOrComputed(element, property).trim();
|
|
4221
|
-
return value === "none" ? "" : value;
|
|
4222
|
-
};
|
|
4223
|
-
const preparePathOffsetBase = (element) => {
|
|
4224
|
-
const currentTranslate = readTransformLonghandBase(element, "translate");
|
|
4225
|
-
const hasMarker = element.hasAttribute(PATH_OFFSET_ATTR);
|
|
4226
|
-
const wasResetByAnimation = !styleUsesStudioOffset(currentTranslate);
|
|
4227
|
-
if (!hasMarker) {
|
|
4228
|
-
element.setAttribute(ORIGINAL_TRANSLATE_ATTR, wasResetByAnimation ? currentTranslate : "");
|
|
4229
|
-
} else if (wasResetByAnimation) {
|
|
4230
|
-
element.setAttribute(ORIGINAL_TRANSLATE_ATTR, currentTranslate);
|
|
4231
|
-
}
|
|
4232
|
-
};
|
|
4233
|
-
const prepareRotationBase = (element) => {
|
|
4234
|
-
const currentRotate = readTransformLonghandBase(element, "rotate");
|
|
4235
|
-
const hasMarker = element.hasAttribute(ROTATION_ATTR);
|
|
4236
|
-
const wasResetByAnimation = !styleUsesStudioRotation(currentRotate);
|
|
4237
|
-
if (!hasMarker) {
|
|
4238
|
-
element.setAttribute(ORIGINAL_ROTATE_ATTR, wasResetByAnimation ? currentRotate : "");
|
|
4239
|
-
} else if (wasResetByAnimation) {
|
|
4240
|
-
element.setAttribute(ORIGINAL_ROTATE_ATTR, currentRotate);
|
|
4241
|
-
}
|
|
4242
|
-
};
|
|
4243
|
-
const querySelectorCandidates = (selector) => {
|
|
4244
|
-
const isCandidate = (element) => element instanceof HTMLElement;
|
|
4245
|
-
const className = selector.match(/^\.([A-Za-z0-9_-]+)$/)?.[1];
|
|
4246
|
-
if (className) {
|
|
4247
|
-
return Array.from(document.getElementsByTagName("*")).filter(
|
|
4248
|
-
(element) => isCandidate(element) && element.classList.contains(className)
|
|
4249
|
-
);
|
|
4250
|
-
}
|
|
4251
|
-
if (/^[A-Za-z][A-Za-z0-9-]*$/.test(selector)) {
|
|
4252
|
-
return Array.from(document.getElementsByTagName(selector)).filter(isCandidate);
|
|
4253
|
-
}
|
|
4254
|
-
return Array.from(document.querySelectorAll(selector)).filter(isCandidate);
|
|
4255
|
-
};
|
|
4256
|
-
const resolveTarget = (edit) => {
|
|
4257
|
-
const targetRecord = objectRecord(edit.target);
|
|
4258
|
-
if (!targetRecord) return null;
|
|
4259
|
-
const sourceFile = typeof targetRecord.sourceFile === "string" ? targetRecord.sourceFile : "";
|
|
4260
|
-
if (!sourceFile) return null;
|
|
4261
|
-
const id = typeof targetRecord.id === "string" ? targetRecord.id : "";
|
|
4262
|
-
if (id) {
|
|
4263
|
-
const byId = document.getElementById(id);
|
|
4264
|
-
if (byId instanceof HTMLElement && elementMatchesSourceFile(byId, sourceFile)) return byId;
|
|
4265
|
-
const matchesById = [
|
|
4266
|
-
document.documentElement,
|
|
4267
|
-
...Array.from(document.getElementsByTagName("*"))
|
|
4268
|
-
].filter(
|
|
4269
|
-
(element) => element instanceof HTMLElement && element.id === id && elementMatchesSourceFile(element, sourceFile)
|
|
4270
|
-
);
|
|
4271
|
-
if (matchesById[0]) return matchesById[0];
|
|
4272
|
-
}
|
|
4273
|
-
const selector = typeof targetRecord.selector === "string" ? targetRecord.selector : "";
|
|
4274
|
-
if (!selector) return null;
|
|
4275
|
-
try {
|
|
4276
|
-
const matches = querySelectorCandidates(selector).filter(
|
|
4277
|
-
(element) => elementMatchesSourceFile(element, sourceFile)
|
|
4278
|
-
);
|
|
4279
|
-
const selectorIndex = finiteNumber(targetRecord.selectorIndex) ?? 0;
|
|
4280
|
-
return matches[Math.max(0, Math.floor(selectorIndex))] ?? null;
|
|
4281
|
-
} catch {
|
|
4282
|
-
return null;
|
|
4283
|
-
}
|
|
4284
|
-
};
|
|
4285
|
-
const roundRotationAngle = (angle) => Math.round(angle * 10) / 10;
|
|
4286
|
-
const isSimpleRotateAngle = (value) => /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:deg|rad|turn|grad)$/.test(value.trim());
|
|
4287
|
-
const composeRotation = (element, rotationValue) => {
|
|
4288
|
-
const original = element.getAttribute(ORIGINAL_ROTATE_ATTR)?.trim();
|
|
4289
|
-
if (!original || original === "none" || !isSimpleRotateAngle(original)) {
|
|
4290
|
-
return rotationValue;
|
|
4291
|
-
}
|
|
4292
|
-
return `calc(${original} + ${rotationValue})`;
|
|
4293
|
-
};
|
|
4294
|
-
const applyPathOffset = (element, edit) => {
|
|
4295
|
-
const x = finiteNumber(edit.x);
|
|
4296
|
-
const y = finiteNumber(edit.y);
|
|
4297
|
-
if (x == null || y == null) return;
|
|
4298
|
-
preparePathOffsetBase(element);
|
|
4299
|
-
element.setAttribute(PATH_OFFSET_ATTR, "true");
|
|
4300
|
-
element.style.setProperty(OFFSET_X_PROP, `${Math.round(x)}px`);
|
|
4301
|
-
element.style.setProperty(OFFSET_Y_PROP, `${Math.round(y)}px`);
|
|
4302
|
-
element.style.setProperty(
|
|
4303
|
-
"translate",
|
|
4304
|
-
composeTranslate(element, `var(${OFFSET_X_PROP}, 0px)`, `var(${OFFSET_Y_PROP}, 0px)`)
|
|
4305
|
-
);
|
|
4306
|
-
};
|
|
4307
|
-
const readParentFlexBasisPixels = (element, size) => {
|
|
4308
|
-
const parent = element.parentElement;
|
|
4309
|
-
if (!parent) return null;
|
|
4310
|
-
const styles = getComputedStyle(parent);
|
|
4311
|
-
if (styles.display !== "flex" && styles.display !== "inline-flex") return null;
|
|
4312
|
-
return Math.round(
|
|
4313
|
-
Math.max(1, styles.flexDirection.startsWith("column") ? size.height : size.width)
|
|
4314
|
-
);
|
|
4315
|
-
};
|
|
4316
|
-
const applyBoxSize = (element, edit) => {
|
|
4317
|
-
const width = finiteNumber(edit.width);
|
|
4318
|
-
const height = finiteNumber(edit.height);
|
|
4319
|
-
if (width == null || height == null || width <= 0 || height <= 0) return;
|
|
4320
|
-
const rounded = {
|
|
4321
|
-
width: Math.round(Math.max(1, width)),
|
|
4322
|
-
height: Math.round(Math.max(1, height))
|
|
4323
|
-
};
|
|
4324
|
-
element.setAttribute(BOX_SIZE_ATTR, "true");
|
|
4325
|
-
element.style.setProperty(WIDTH_PROP, `${rounded.width}px`);
|
|
4326
|
-
element.style.setProperty(HEIGHT_PROP, `${rounded.height}px`);
|
|
4327
|
-
element.style.setProperty("box-sizing", "border-box");
|
|
4328
|
-
element.style.setProperty("width", `${rounded.width}px`);
|
|
4329
|
-
element.style.setProperty("height", `${rounded.height}px`);
|
|
4330
|
-
element.style.setProperty("min-width", "0px");
|
|
4331
|
-
element.style.setProperty("min-height", "0px");
|
|
4332
|
-
element.style.setProperty("max-width", "none");
|
|
4333
|
-
element.style.setProperty("max-height", "none");
|
|
4334
|
-
const flexBasis = readParentFlexBasisPixels(element, rounded);
|
|
4335
|
-
if (flexBasis != null) {
|
|
4336
|
-
element.style.setProperty("flex-basis", `${flexBasis}px`);
|
|
4337
|
-
element.style.setProperty("flex-grow", "0");
|
|
4338
|
-
element.style.setProperty("flex-shrink", "0");
|
|
4339
|
-
}
|
|
4340
|
-
if (getComputedStyle(element).display === "inline") {
|
|
4341
|
-
element.style.setProperty("display", "inline-block");
|
|
4342
|
-
}
|
|
4343
|
-
};
|
|
4344
|
-
const applyRotation = (element, edit) => {
|
|
4345
|
-
const angle = finiteNumber(edit.angle);
|
|
4346
|
-
if (angle == null) return;
|
|
4347
|
-
prepareRotationBase(element);
|
|
4348
|
-
element.setAttribute(ROTATION_ATTR, "true");
|
|
4349
|
-
element.style.setProperty(ROTATION_PROP, `${roundRotationAngle(angle)}deg`);
|
|
4350
|
-
element.style.setProperty("transform-origin", ROTATION_TRANSFORM_ORIGIN);
|
|
4351
|
-
element.style.setProperty("rotate", composeRotation(element, `var(${ROTATION_PROP}, 0deg)`));
|
|
4352
|
-
};
|
|
4353
|
-
const applyManifest = () => {
|
|
4354
|
-
let applied = 0;
|
|
4355
|
-
for (const edit of manifestEdits) {
|
|
4356
|
-
const editRecord = objectRecord(edit);
|
|
4357
|
-
if (!editRecord) continue;
|
|
4358
|
-
const element = resolveTarget(editRecord);
|
|
4359
|
-
if (!element) continue;
|
|
4360
|
-
if (editRecord.kind === "path-offset") applyPathOffset(element, editRecord);
|
|
4361
|
-
if (editRecord.kind === "box-size") applyBoxSize(element, editRecord);
|
|
4362
|
-
if (editRecord.kind === "rotation") applyRotation(element, editRecord);
|
|
4363
|
-
applied += 1;
|
|
4364
|
-
}
|
|
4365
|
-
return applied;
|
|
4366
|
-
};
|
|
4367
|
-
runtimeWindow.__hfStudioManualEditsApply = applyManifest;
|
|
4368
|
-
const markWrapped = (fn) => {
|
|
4369
|
-
try {
|
|
4370
|
-
Object.defineProperty(fn, WRAPPED_SEEK_PROP, {
|
|
4371
|
-
configurable: false,
|
|
4372
|
-
enumerable: false,
|
|
4373
|
-
value: true
|
|
4374
|
-
});
|
|
4375
|
-
} catch {
|
|
4376
|
-
try {
|
|
4377
|
-
fn[WRAPPED_SEEK_PROP] = true;
|
|
4378
|
-
} catch {
|
|
4379
|
-
}
|
|
4380
|
-
}
|
|
4381
|
-
};
|
|
4382
|
-
const isWrapped = (fn) => Boolean(fn[WRAPPED_SEEK_PROP]);
|
|
4383
|
-
const wrapFunction = (get, set) => {
|
|
4384
|
-
const fn = get();
|
|
4385
|
-
if (!fn) return false;
|
|
4386
|
-
const seek = fn;
|
|
4387
|
-
if (isWrapped(seek)) {
|
|
4388
|
-
applyManifest();
|
|
4389
|
-
return true;
|
|
4390
|
-
}
|
|
4391
|
-
const wrappedSeek = function(time) {
|
|
4392
|
-
const result = seek.call(this, time);
|
|
4393
|
-
applyManifest();
|
|
4394
|
-
return result;
|
|
4395
|
-
};
|
|
4396
|
-
markWrapped(wrappedSeek);
|
|
4397
|
-
set(wrappedSeek);
|
|
4398
|
-
applyManifest();
|
|
4399
|
-
return true;
|
|
4400
|
-
};
|
|
4401
|
-
const wrapSeekFunctions = () => {
|
|
4402
|
-
const wrappedHfSeek = wrapFunction(
|
|
4403
|
-
() => runtimeWindow.__hf?.seek,
|
|
4404
|
-
(fn) => {
|
|
4405
|
-
if (runtimeWindow.__hf) runtimeWindow.__hf.seek = fn;
|
|
4406
|
-
}
|
|
4407
|
-
);
|
|
4408
|
-
const wrappedPlayerRenderSeek = wrapFunction(
|
|
4409
|
-
() => runtimeWindow.__player?.renderSeek,
|
|
4410
|
-
(fn) => {
|
|
4411
|
-
if (runtimeWindow.__player) runtimeWindow.__player.renderSeek = fn;
|
|
4412
|
-
}
|
|
4413
|
-
);
|
|
4414
|
-
return wrappedHfSeek || wrappedPlayerRenderSeek;
|
|
4415
|
-
};
|
|
4416
|
-
if (document.readyState === "loading") {
|
|
4417
|
-
document.addEventListener("DOMContentLoaded", () => applyManifest(), { once: true });
|
|
4418
|
-
} else {
|
|
4419
|
-
applyManifest();
|
|
4420
|
-
}
|
|
4421
|
-
wrapSeekFunctions();
|
|
4422
|
-
let remainingSeekWrapAttempts = 120;
|
|
4423
|
-
const seekWrapInterval = setInterval(() => {
|
|
4424
|
-
wrapSeekFunctions();
|
|
4425
|
-
remainingSeekWrapAttempts -= 1;
|
|
4426
|
-
if (remainingSeekWrapAttempts <= 0) clearInterval(seekWrapInterval);
|
|
4427
|
-
}, 50);
|
|
4428
|
-
}
|
|
4429
|
-
|
|
4430
|
-
// src/routes/thumbnail.ts
|
|
4431
3782
|
var THUMBNAIL_CACHE_VERSION = "v4";
|
|
4432
3783
|
function registerThumbnailRoutes(api, adapter) {
|
|
4433
3784
|
api.get("/projects/:id/thumbnail/*", async (c) => {
|
|
@@ -4457,8 +3808,8 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4457
3808
|
let sourceMtime = 0;
|
|
4458
3809
|
let sourceKey = "";
|
|
4459
3810
|
const htmlFile = join11(project.dir, compPath);
|
|
4460
|
-
if (
|
|
4461
|
-
const html =
|
|
3811
|
+
if (existsSync8(htmlFile)) {
|
|
3812
|
+
const html = readFileSync11(htmlFile, "utf-8");
|
|
4462
3813
|
sourceKey = `_${createHash4("sha1").update(html).digest("hex").slice(0, 16)}`;
|
|
4463
3814
|
sourceMtime = Math.round(statSync4(htmlFile).mtimeMs);
|
|
4464
3815
|
if (!vpWidth) {
|
|
@@ -4470,15 +3821,15 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4470
3821
|
}
|
|
4471
3822
|
const manualEditsFile = join11(project.dir, STUDIO_MANUAL_EDITS_PATH);
|
|
4472
3823
|
let manualEditsKey = "";
|
|
4473
|
-
if (
|
|
4474
|
-
const manualEditsContent =
|
|
3824
|
+
if (existsSync8(manualEditsFile)) {
|
|
3825
|
+
const manualEditsContent = readFileSync11(manualEditsFile, "utf-8");
|
|
4475
3826
|
manualEditsKey = `_${createHash4("sha1").update(manualEditsContent).digest("hex").slice(0, 16)}`;
|
|
4476
3827
|
sourceMtime = Math.max(sourceMtime, Math.round(statSync4(manualEditsFile).mtimeMs));
|
|
4477
3828
|
}
|
|
4478
3829
|
const motionFile = join11(project.dir, STUDIO_MOTION_PATH);
|
|
4479
3830
|
let motionKey = "";
|
|
4480
|
-
if (
|
|
4481
|
-
const motionContent =
|
|
3831
|
+
if (existsSync8(motionFile)) {
|
|
3832
|
+
const motionContent = readFileSync11(motionFile, "utf-8");
|
|
4482
3833
|
motionKey = `_${createHash4("sha1").update(motionContent).digest("hex").slice(0, 16)}`;
|
|
4483
3834
|
sourceMtime = Math.max(sourceMtime, Math.round(statSync4(motionFile).mtimeMs));
|
|
4484
3835
|
}
|
|
@@ -4488,8 +3839,8 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4488
3839
|
const urlVersionKey = urlVersion ? `_${urlVersion.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32)}` : "";
|
|
4489
3840
|
const cacheKey = `${THUMBNAIL_CACHE_VERSION}${urlVersionKey}${manualEditsKey}${motionKey}${sourceKey}_${format}_${compPath.replace(/\//g, "_")}_${compW}x${compH}_${sourceMtime}_${seekTime.toFixed(2)}${selectorKey}.${format === "png" ? "png" : "jpg"}`;
|
|
4490
3841
|
const cachePath = join11(cacheDir, cacheKey);
|
|
4491
|
-
if (
|
|
4492
|
-
return new Response(new Uint8Array(
|
|
3842
|
+
if (existsSync8(cachePath)) {
|
|
3843
|
+
return new Response(new Uint8Array(readFileSync11(cachePath)), {
|
|
4493
3844
|
headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
|
|
4494
3845
|
});
|
|
4495
3846
|
}
|
|
@@ -4511,7 +3862,7 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4511
3862
|
500
|
|
4512
3863
|
);
|
|
4513
3864
|
}
|
|
4514
|
-
if (!
|
|
3865
|
+
if (!existsSync8(cacheDir)) mkdirSync5(cacheDir, { recursive: true });
|
|
4515
3866
|
writeFileSync6(cachePath, buffer);
|
|
4516
3867
|
return new Response(new Uint8Array(buffer), {
|
|
4517
3868
|
headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
|
|
@@ -4524,7 +3875,7 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4524
3875
|
}
|
|
4525
3876
|
|
|
4526
3877
|
// src/routes/waveform.ts
|
|
4527
|
-
import { existsSync as
|
|
3878
|
+
import { existsSync as existsSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6 } from "fs";
|
|
4528
3879
|
import { join as join12 } from "path";
|
|
4529
3880
|
function registerWaveformRoutes(api, adapter) {
|
|
4530
3881
|
api.get("/projects/:id/waveform/*", async (c) => {
|
|
@@ -4534,12 +3885,12 @@ function registerWaveformRoutes(api, adapter) {
|
|
|
4534
3885
|
c.req.path.replace(`/projects/${project.id}/waveform/`, "").split("?")[0] ?? ""
|
|
4535
3886
|
);
|
|
4536
3887
|
const audioPath = join12(project.dir, assetPath);
|
|
4537
|
-
if (!
|
|
3888
|
+
if (!existsSync9(audioPath)) return c.json({ error: "file not found" }, 404);
|
|
4538
3889
|
const cacheDir = join12(project.dir, ".waveform-cache");
|
|
4539
3890
|
const cachePath = join12(cacheDir, buildWaveformCacheKey(assetPath));
|
|
4540
|
-
if (
|
|
3891
|
+
if (existsSync9(cachePath)) {
|
|
4541
3892
|
try {
|
|
4542
|
-
const peaks2 = JSON.parse(
|
|
3893
|
+
const peaks2 = JSON.parse(readFileSync12(cachePath, "utf-8"));
|
|
4543
3894
|
return c.json({ peaks: peaks2 });
|
|
4544
3895
|
} catch {
|
|
4545
3896
|
}
|
|
@@ -4823,102 +4174,29 @@ function registerSelectionRoutes(api, adapter) {
|
|
|
4823
4174
|
|
|
4824
4175
|
// src/routes/media.ts
|
|
4825
4176
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
4826
|
-
import { existsSync as
|
|
4827
|
-
import { basename as basename2, dirname as
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
if (VIDEO_EXT2.has(ext)) return "video";
|
|
4841
|
-
if (IMAGE_EXT.has(ext)) return "image";
|
|
4842
|
-
if (AUDIO_EXT2.has(ext)) return "audio";
|
|
4843
|
-
return "unknown";
|
|
4844
|
-
}
|
|
4845
|
-
function colorLabel(input) {
|
|
4846
|
-
if (input.isHdr) {
|
|
4847
|
-
if (input.hdrTransfer === "pq") return "HDR PQ";
|
|
4848
|
-
if (input.hdrTransfer === "hlg") return "HDR HLG";
|
|
4849
|
-
return "HDR";
|
|
4850
|
-
}
|
|
4851
|
-
if (input.colorPrimaries.includes("bt709") || input.colorSpace.includes("bt709") || input.colorTransfer.includes("bt709")) {
|
|
4852
|
-
return "SDR Rec.709";
|
|
4853
|
-
}
|
|
4854
|
-
return "SDR/unknown";
|
|
4855
|
-
}
|
|
4856
|
-
function classifyMediaColor(stream) {
|
|
4857
|
-
const colorPrimaries = lower(stream?.color_primaries);
|
|
4858
|
-
const colorSpace = lower(stream?.color_space);
|
|
4859
|
-
const colorTransfer = lower(stream?.color_transfer);
|
|
4860
|
-
const isHdr = colorPrimaries.includes("bt2020") || colorSpace.includes("bt2020") || colorTransfer === "smpte2084" || colorTransfer === "arib-std-b67";
|
|
4861
|
-
const hdrTransfer = isHdr ? colorTransfer === "smpte2084" ? "pq" : colorTransfer === "arib-std-b67" ? "hlg" : "unknown" : null;
|
|
4862
|
-
return {
|
|
4863
|
-
dynamicRange: stream ? isHdr ? "hdr" : "sdr" : "unknown",
|
|
4864
|
-
hdrTransfer,
|
|
4865
|
-
label: stream ? colorLabel({ isHdr, hdrTransfer, colorPrimaries, colorSpace, colorTransfer }) : "Unknown",
|
|
4866
|
-
isHdr,
|
|
4867
|
-
codecName: stream?.codec_name,
|
|
4868
|
-
profile: stream?.profile,
|
|
4869
|
-
pixelFormat: stream?.pix_fmt,
|
|
4870
|
-
colorSpace: stream?.color_space,
|
|
4871
|
-
colorTransfer: stream?.color_transfer,
|
|
4872
|
-
colorPrimaries: stream?.color_primaries,
|
|
4873
|
-
bitsPerRawSample: stream?.bits_per_raw_sample
|
|
4874
|
-
};
|
|
4875
|
-
}
|
|
4876
|
-
function probeMediaMetadata(filePath, runner = spawnSync2) {
|
|
4877
|
-
const kind = inferKindFromPath(filePath);
|
|
4878
|
-
if (kind === "audio" || kind === "unknown") {
|
|
4879
|
-
return { kind, color: classifyMediaColor(null) };
|
|
4880
|
-
}
|
|
4881
|
-
const result = runner(
|
|
4882
|
-
"ffprobe",
|
|
4883
|
-
[
|
|
4884
|
-
"-v",
|
|
4885
|
-
"error",
|
|
4886
|
-
"-show_entries",
|
|
4887
|
-
"stream=codec_type,codec_name,profile,pix_fmt,color_space,color_transfer,color_primaries,bits_per_raw_sample",
|
|
4888
|
-
"-of",
|
|
4889
|
-
"json",
|
|
4890
|
-
filePath
|
|
4891
|
-
],
|
|
4892
|
-
{ timeout: 15e3, maxBuffer: 1024 * 1024 }
|
|
4893
|
-
);
|
|
4894
|
-
if (result.error?.code === "ENOENT") {
|
|
4895
|
-
return { kind, color: classifyMediaColor(null), probeError: "ffprobe unavailable" };
|
|
4896
|
-
}
|
|
4897
|
-
if (result.status !== 0) {
|
|
4898
|
-
return { kind, color: classifyMediaColor(null), probeError: "ffprobe failed" };
|
|
4899
|
-
}
|
|
4900
|
-
try {
|
|
4901
|
-
const parsed = JSON.parse(String(result.stdout || "{}"));
|
|
4902
|
-
const stream = parsed.streams?.find(
|
|
4903
|
-
(item) => kind === "image" ? item.codec_type === "video" : item.codec_type === kind
|
|
4904
|
-
);
|
|
4905
|
-
return { kind, color: classifyMediaColor(stream) };
|
|
4906
|
-
} catch {
|
|
4907
|
-
return { kind, color: classifyMediaColor(null), probeError: "ffprobe returned invalid json" };
|
|
4908
|
-
}
|
|
4909
|
-
}
|
|
4910
|
-
|
|
4911
|
-
// src/routes/media.ts
|
|
4912
|
-
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".mov", ".webm", ".mkv", ".avi"]);
|
|
4177
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7 } from "fs";
|
|
4178
|
+
import { basename as basename2, dirname as dirname3, extname as extname2, join as join13 } from "path";
|
|
4179
|
+
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
4180
|
+
".mp4",
|
|
4181
|
+
".mov",
|
|
4182
|
+
".webm",
|
|
4183
|
+
".mkv",
|
|
4184
|
+
".avi",
|
|
4185
|
+
".m4v",
|
|
4186
|
+
".mxf",
|
|
4187
|
+
".mts",
|
|
4188
|
+
".m2ts",
|
|
4189
|
+
".ts"
|
|
4190
|
+
]);
|
|
4913
4191
|
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".webp"]);
|
|
4914
4192
|
var VIDEO_OUTPUT_EXTENSIONS = /* @__PURE__ */ new Set([".webm", ".mov"]);
|
|
4915
4193
|
var QUALITIES = /* @__PURE__ */ new Set(["fast", "balanced", "best"]);
|
|
4916
4194
|
var DEVICES = /* @__PURE__ */ new Set(["auto", "cpu", "coreml", "cuda"]);
|
|
4917
4195
|
function isVideoPath(path) {
|
|
4918
|
-
return VIDEO_EXTENSIONS.has(
|
|
4196
|
+
return VIDEO_EXTENSIONS.has(extname2(path).toLowerCase());
|
|
4919
4197
|
}
|
|
4920
4198
|
function isImagePath(path) {
|
|
4921
|
-
return IMAGE_EXTENSIONS.has(
|
|
4199
|
+
return IMAGE_EXTENSIONS.has(extname2(path).toLowerCase());
|
|
4922
4200
|
}
|
|
4923
4201
|
function normalizeProjectAssetPath(path) {
|
|
4924
4202
|
return path.trim().replace(/^[.]\//, "").replace(/[?#].*$/, "");
|
|
@@ -4927,14 +4205,14 @@ function containsNullByte(path) {
|
|
|
4927
4205
|
return path.includes("\0");
|
|
4928
4206
|
}
|
|
4929
4207
|
function slugFileBase(path) {
|
|
4930
|
-
const name = basename2(path,
|
|
4208
|
+
const name = basename2(path, extname2(path)).replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
4931
4209
|
return name || "media";
|
|
4932
4210
|
}
|
|
4933
4211
|
function uniqueAssetPath(projectDir, assetPath) {
|
|
4934
|
-
const ext =
|
|
4212
|
+
const ext = extname2(assetPath);
|
|
4935
4213
|
const withoutExt = assetPath.slice(0, -ext.length);
|
|
4936
4214
|
let candidate = assetPath;
|
|
4937
|
-
for (let index = 2;
|
|
4215
|
+
for (let index = 2; existsSync10(join13(projectDir, candidate)); index++) {
|
|
4938
4216
|
candidate = `${withoutExt}-${index}${ext}`;
|
|
4939
4217
|
}
|
|
4940
4218
|
return candidate;
|
|
@@ -4985,8 +4263,8 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
4985
4263
|
}
|
|
4986
4264
|
const filePath = resolveWithinProject(project.dir, assetPath);
|
|
4987
4265
|
if (!filePath) return c.json({ error: "forbidden" }, 403);
|
|
4988
|
-
if (!
|
|
4989
|
-
return c.json({ path: assetPath, metadata: readMediaMetadata(filePath) });
|
|
4266
|
+
if (!existsSync10(filePath)) return c.json({ error: "media not found" }, 404);
|
|
4267
|
+
return c.json({ path: assetPath, metadata: await readMediaMetadata(filePath) });
|
|
4990
4268
|
});
|
|
4991
4269
|
api.post(
|
|
4992
4270
|
"/projects/:id/media/remove-background",
|
|
@@ -5007,7 +4285,7 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5007
4285
|
}
|
|
5008
4286
|
const inputPath = resolveWithinProject(project.dir, inputAssetPath);
|
|
5009
4287
|
if (!inputPath) return c.json({ error: "forbidden" }, 403);
|
|
5010
|
-
if (!
|
|
4288
|
+
if (!existsSync10(inputPath)) return c.json({ error: "input media not found" }, 404);
|
|
5011
4289
|
const inputIsVideo = isVideoPath(inputAssetPath);
|
|
5012
4290
|
const inputIsImage = isImagePath(inputAssetPath);
|
|
5013
4291
|
if (!inputIsVideo && !inputIsImage) {
|
|
@@ -5023,10 +4301,10 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5023
4301
|
const outputAssetPath = requestedOutput ? uniqueAssetPath(project.dir, requestedOutput) : defaultOutputPath(project.dir, inputAssetPath);
|
|
5024
4302
|
const outputPath = resolveWithinProject(project.dir, outputAssetPath);
|
|
5025
4303
|
if (!outputPath) return c.json({ error: "forbidden" }, 403);
|
|
5026
|
-
if (inputIsVideo && !VIDEO_OUTPUT_EXTENSIONS.has(
|
|
4304
|
+
if (inputIsVideo && !VIDEO_OUTPUT_EXTENSIONS.has(extname2(outputAssetPath).toLowerCase())) {
|
|
5027
4305
|
return c.json({ error: "video background removal output must be .webm or .mov" }, 400);
|
|
5028
4306
|
}
|
|
5029
|
-
if (inputIsImage &&
|
|
4307
|
+
if (inputIsImage && extname2(outputAssetPath).toLowerCase() !== ".png") {
|
|
5030
4308
|
return c.json({ error: "image background removal output must be .png" }, 400);
|
|
5031
4309
|
}
|
|
5032
4310
|
let backgroundOutputAssetPath;
|
|
@@ -5041,8 +4319,8 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5041
4319
|
return c.json({ error: "forbidden" }, 403);
|
|
5042
4320
|
}
|
|
5043
4321
|
}
|
|
5044
|
-
mkdirSync7(
|
|
5045
|
-
if (backgroundOutputPath) mkdirSync7(
|
|
4322
|
+
mkdirSync7(dirname3(outputPath), { recursive: true });
|
|
4323
|
+
if (backgroundOutputPath) mkdirSync7(dirname3(backgroundOutputPath), { recursive: true });
|
|
5046
4324
|
const jobId = makeJobId(project.id, mediaJobs);
|
|
5047
4325
|
const state = adapter.startBackgroundRemoval({
|
|
5048
4326
|
project,
|
|
@@ -5099,14 +4377,14 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5099
4377
|
}
|
|
5100
4378
|
|
|
5101
4379
|
// src/routes/globalAssets.ts
|
|
5102
|
-
import { existsSync as
|
|
4380
|
+
import { existsSync as existsSync11, readFileSync as readFileSync13 } from "fs";
|
|
5103
4381
|
import { homedir } from "os";
|
|
5104
4382
|
import { join as join14 } from "path";
|
|
5105
4383
|
function readGlobalAssets(home = homedir()) {
|
|
5106
4384
|
const manifestPath = join14(home, ".media", "manifest.jsonl");
|
|
5107
|
-
if (!
|
|
4385
|
+
if (!existsSync11(manifestPath)) return [];
|
|
5108
4386
|
const out = [];
|
|
5109
|
-
for (const line of
|
|
4387
|
+
for (const line of readFileSync13(manifestPath, "utf8").split("\n")) {
|
|
5110
4388
|
if (!line.trim()) continue;
|
|
5111
4389
|
try {
|
|
5112
4390
|
const rec = JSON.parse(line);
|
|
@@ -5142,29 +4420,6 @@ function createStudioApi(adapter) {
|
|
|
5142
4420
|
return api;
|
|
5143
4421
|
}
|
|
5144
4422
|
|
|
5145
|
-
// src/helpers/screenshotClip.ts
|
|
5146
|
-
function getElementScreenshotClip(selector, selectorIndex) {
|
|
5147
|
-
const matches = Array.from(document.querySelectorAll(selector)).filter(
|
|
5148
|
-
(el2) => el2 instanceof HTMLElement
|
|
5149
|
-
);
|
|
5150
|
-
const safeIndex = Math.max(0, Math.min(matches.length - 1, Math.floor(selectorIndex ?? 0)));
|
|
5151
|
-
const el = matches[safeIndex] ?? null;
|
|
5152
|
-
if (!(el instanceof HTMLElement)) return void 0;
|
|
5153
|
-
const rect = el.getBoundingClientRect();
|
|
5154
|
-
if (rect.width < 4 || rect.height < 4) return void 0;
|
|
5155
|
-
const pad = 8;
|
|
5156
|
-
const x = Math.max(0, rect.left - pad);
|
|
5157
|
-
const y = Math.max(0, rect.top - pad);
|
|
5158
|
-
const maxWidth = window.innerWidth - x;
|
|
5159
|
-
const maxHeight = window.innerHeight - y;
|
|
5160
|
-
return {
|
|
5161
|
-
x,
|
|
5162
|
-
y,
|
|
5163
|
-
width: Math.max(1, Math.min(rect.width + pad * 2, maxWidth)),
|
|
5164
|
-
height: Math.max(1, Math.min(rect.height + pad * 2, maxHeight))
|
|
5165
|
-
};
|
|
5166
|
-
}
|
|
5167
|
-
|
|
5168
4423
|
// src/helpers/backgroundRemovalJob.ts
|
|
5169
4424
|
function createBackgroundRemovalJob(opts, render) {
|
|
5170
4425
|
const state = {
|