@hyperframes/studio 0.6.117 → 0.6.118
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/assets/{index-CWpmpjkU.js → index-CUtjhezZ.js} +94 -94
- package/dist/assets/{index-BEoZ8owy.js → index-Ct7l2mBV.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +5 -5
- package/src/contexts/FileManagerContext.tsx +4 -0
- package/src/hooks/useMusicBeatAnalysis.ts +23 -7
- package/src/player/components/Timeline.test.ts +41 -0
- package/src/player/components/TimelineCanvas.tsx +2 -2
- package/src/player/components/useResolvedTimelineEditCallbacks.ts +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
import{g as P}from"./index-
|
|
1
|
+
import{g as P}from"./index-CUtjhezZ.js";function j(c,d){for(var s=0;s<d.length;s++){const a=d[s];if(typeof a!="string"&&!Array.isArray(a)){for(const i in a)if(i!=="default"&&!(i in c)){const l=Object.getOwnPropertyDescriptor(a,i);l&&Object.defineProperty(c,i,l.get?l:{enumerable:!0,get:()=>a[i]})}}}return Object.freeze(Object.defineProperty(c,Symbol.toStringTag,{value:"Module"}))}var v={},w;function k(){if(w)return v;w=1,Object.defineProperty(v,"__esModule",{value:!0}),v.default=d;var c=window.OfflineAudioContext||window.webkitOfflineAudioContext;function d(e){var r=a(e);return r.start(0),[i,y,O(e.sampleRate),s].reduce(function(t,o){return o(t)},r.buffer.getChannelData(0))}function s(e){return e.sort(function(r,t){return t.count-r.count}).splice(0,5)[0].tempo}function a(e){var r=e.length,t=e.numberOfChannels,o=e.sampleRate,n=new c(t,r,o),u=n.createBufferSource();u.buffer=e;var f=n.createBiquadFilter();return f.type="lowpass",u.connect(f),f.connect(n.destination),u}function i(e){for(var r=[],t=.9,o=.3,n=15;r.length<n&&t>=o;)r=l(e,t),t-=.05;if(r.length<n)throw new Error("Could not find enough samples for a reliable detection.");return r}function l(e,r){for(var t=[],o=0,n=e.length;o<n;o+=1)e[o]>r&&(t.push(o),o+=1e4);return t}function y(e){var r=[];return e.forEach(function(t,o){for(var n=function(x){var g=e[o+x]-t,_=r.some(function(h){if(h.interval===g)return h.count+=1});_||r.push({interval:g,count:1})},u=0;u<10;u+=1)n(u)}),r}function O(e){return function(r){var t=[];return r.forEach(function(o){if(o.interval!==0){for(var n=60/(o.interval/e);n<90;)n*=2;for(;n>180;)n/=2;n=Math.round(n);var u=t.some(function(f){if(f.tempo===n)return f.count+=o.count});u||t.push({tempo:n,count:o.count})}}),t}}return v}var p,b;function q(){return b||(b=1,p=k().default),p}var m=q();const A=P(m),D=j({__proto__:null,default:A},[m]);export{D as i};
|
package/dist/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
7
|
<title>HyperFrames Studio</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-CUtjhezZ.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-BvEhf7SS.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.118",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"dompurify": "^3.2.4",
|
|
37
37
|
"marked": "^14.1.4",
|
|
38
38
|
"mediabunny": "^1.45.3",
|
|
39
|
-
"@hyperframes/core": "0.6.
|
|
40
|
-
"@hyperframes/
|
|
41
|
-
"@hyperframes/
|
|
39
|
+
"@hyperframes/core": "0.6.118",
|
|
40
|
+
"@hyperframes/player": "0.6.118",
|
|
41
|
+
"@hyperframes/sdk": "0.6.118"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/react": "19",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"vite": "^6.4.2",
|
|
53
53
|
"vitest": "^3.2.4",
|
|
54
54
|
"zustand": "^5.0.0",
|
|
55
|
-
"@hyperframes/producer": "0.6.
|
|
55
|
+
"@hyperframes/producer": "0.6.118"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"react": "19",
|
|
@@ -11,6 +11,10 @@ export function useFileManagerContext(): FileManagerValue {
|
|
|
11
11
|
return ctx;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export function useFileManagerContextOptional(): FileManagerValue | null {
|
|
15
|
+
return useContext(FileManagerContext);
|
|
16
|
+
}
|
|
17
|
+
|
|
14
18
|
export function FileManagerProvider({
|
|
15
19
|
value: {
|
|
16
20
|
editingFile,
|
|
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef } from "react";
|
|
|
2
2
|
import { usePlayerStore } from "../player/store/playerStore";
|
|
3
3
|
import { isMusicTrack } from "../utils/timelineInspector";
|
|
4
4
|
import { analyzeMusicFromUrl } from "@hyperframes/core/beats";
|
|
5
|
-
import {
|
|
5
|
+
import { useFileManagerContextOptional } from "../contexts/FileManagerContext";
|
|
6
6
|
import { mergeUserBeats } from "../utils/beatEditing";
|
|
7
7
|
import {
|
|
8
8
|
audioRelPathForSrc,
|
|
@@ -58,11 +58,18 @@ export function useMusicBeatAnalysis(): void {
|
|
|
58
58
|
const setBeatEdits = usePlayerStore((s) => s.setBeatEdits);
|
|
59
59
|
const setBeatPersist = usePlayerStore((s) => s.setBeatPersist);
|
|
60
60
|
const resetBeatHistory = usePlayerStore((s) => s.resetBeatHistory);
|
|
61
|
-
const
|
|
61
|
+
const fileManager = useFileManagerContextOptional();
|
|
62
|
+
const readOptionalProjectFile = fileManager?.readOptionalProjectFile;
|
|
63
|
+
const writeProjectFile = fileManager?.writeProjectFile;
|
|
62
64
|
|
|
63
65
|
// File IO via ref so the effects only re-run when the track changes.
|
|
64
|
-
const ioRef = useRef
|
|
65
|
-
|
|
66
|
+
const ioRef = useRef<
|
|
67
|
+
(ProjectIo & { writeProjectFile: (p: string, c: string) => Promise<void> }) | null
|
|
68
|
+
>(null);
|
|
69
|
+
ioRef.current =
|
|
70
|
+
readOptionalProjectFile && writeProjectFile
|
|
71
|
+
? { readOptionalProjectFile, writeProjectFile }
|
|
72
|
+
: null;
|
|
66
73
|
|
|
67
74
|
const musicSrc = useMemo(() => {
|
|
68
75
|
const el = elements.find((e) => isMusicTrack(e));
|
|
@@ -78,6 +85,12 @@ export function useMusicBeatAnalysis(): void {
|
|
|
78
85
|
resetBeatHistory();
|
|
79
86
|
return;
|
|
80
87
|
}
|
|
88
|
+
if (!ioRef.current) {
|
|
89
|
+
setBeatAnalysis(null);
|
|
90
|
+
setBeatEdits(null);
|
|
91
|
+
resetBeatHistory();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
81
94
|
let cancelled = false;
|
|
82
95
|
|
|
83
96
|
let promise = analysisCache.get(musicSrc);
|
|
@@ -90,7 +103,9 @@ export function useMusicBeatAnalysis(): void {
|
|
|
90
103
|
promise
|
|
91
104
|
.then(async (analysis) => {
|
|
92
105
|
const detected = { times: analysis.beatTimes, strengths: analysis.beatStrengths };
|
|
93
|
-
const
|
|
106
|
+
const io = ioRef.current;
|
|
107
|
+
if (!io) return;
|
|
108
|
+
const { times, strengths, hasFile } = await resolveBeats(beatPath, detected, io);
|
|
94
109
|
if (cancelled) return;
|
|
95
110
|
setBeatEdits(null);
|
|
96
111
|
resetBeatHistory();
|
|
@@ -114,10 +129,11 @@ export function useMusicBeatAnalysis(): void {
|
|
|
114
129
|
// Flushes any pending write on cleanup so the last edit is never lost. ──
|
|
115
130
|
useEffect(() => {
|
|
116
131
|
const beatPath = beatFilePathForSrc(musicSrc);
|
|
117
|
-
if (!musicSrc || !beatPath) {
|
|
132
|
+
if (!musicSrc || !beatPath || !ioRef.current) {
|
|
118
133
|
setBeatPersist(null);
|
|
119
134
|
return;
|
|
120
135
|
}
|
|
136
|
+
const io = ioRef.current;
|
|
121
137
|
const audio = audioRelPathForSrc(musicSrc) ?? "audio";
|
|
122
138
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
123
139
|
let pending: string | null = null;
|
|
@@ -126,7 +142,7 @@ export function useMusicBeatAnalysis(): void {
|
|
|
126
142
|
if (pending === null) return;
|
|
127
143
|
const content = pending;
|
|
128
144
|
pending = null;
|
|
129
|
-
void
|
|
145
|
+
void io.writeProjectFile(beatPath, content).catch(() => {});
|
|
130
146
|
};
|
|
131
147
|
|
|
132
148
|
const persist = () => {
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach } from "vitest";
|
|
1
6
|
import { describe, it, expect } from "vitest";
|
|
2
7
|
import {
|
|
8
|
+
Timeline,
|
|
3
9
|
formatTimelineTickLabel,
|
|
4
10
|
generateTicks,
|
|
5
11
|
getDefaultDroppedTrack,
|
|
@@ -14,6 +20,41 @@ import {
|
|
|
14
20
|
} from "./Timeline";
|
|
15
21
|
import { RULER_H, TRACK_H } from "./timelineLayout";
|
|
16
22
|
import { formatTime } from "../lib/time";
|
|
23
|
+
import { usePlayerStore } from "../store/playerStore";
|
|
24
|
+
|
|
25
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
document.body.innerHTML = "";
|
|
29
|
+
usePlayerStore.getState().reset();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("Timeline provider boundary", () => {
|
|
33
|
+
it("renders the public Timeline export without TimelineEditProvider", () => {
|
|
34
|
+
const host = document.createElement("div");
|
|
35
|
+
document.body.append(host);
|
|
36
|
+
Object.defineProperty(host, "clientWidth", {
|
|
37
|
+
configurable: true,
|
|
38
|
+
value: 640,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
usePlayerStore.setState({
|
|
42
|
+
duration: 4,
|
|
43
|
+
timelineReady: true,
|
|
44
|
+
elements: [{ id: "clip-1", tag: "div", start: 0, duration: 2, track: 0 }],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const root = createRoot(host);
|
|
48
|
+
|
|
49
|
+
expect(() => {
|
|
50
|
+
act(() => {
|
|
51
|
+
root.render(React.createElement(Timeline));
|
|
52
|
+
});
|
|
53
|
+
}).not.toThrow();
|
|
54
|
+
|
|
55
|
+
act(() => root.unmount());
|
|
56
|
+
});
|
|
57
|
+
});
|
|
17
58
|
|
|
18
59
|
describe("generateTicks", () => {
|
|
19
60
|
it("returns empty arrays for duration <= 0", () => {
|
|
@@ -21,7 +21,7 @@ import type { DraggedClipState, ResizingClipState, BlockedClipState } from "./us
|
|
|
21
21
|
import type { TrackVisualStyle } from "./timelineIcons";
|
|
22
22
|
import { STUDIO_KEYFRAMES_ENABLED } from "../../components/editor/manualEditingAvailability";
|
|
23
23
|
import { SPLIT_BOUNDARY_EPSILON_S } from "../../utils/timelineElementSplit";
|
|
24
|
-
import {
|
|
24
|
+
import { useTimelineEditContextOptional } from "../../contexts/TimelineEditContext";
|
|
25
25
|
import { isMusicTrack } from "../../utils/timelineInspector";
|
|
26
26
|
|
|
27
27
|
function ClipLabel({ element, color }: { element: TimelineElement; color: string }) {
|
|
@@ -151,7 +151,7 @@ export const TimelineCanvas = memo(function TimelineCanvas({
|
|
|
151
151
|
beatAnalysis,
|
|
152
152
|
}: TimelineCanvasProps) {
|
|
153
153
|
const { onResizeElement, onMoveElement, onRazorSplit, onRazorSplitAll } =
|
|
154
|
-
|
|
154
|
+
useTimelineEditContextOptional();
|
|
155
155
|
const beatDragging = usePlayerStore((s) => s.beatDragging);
|
|
156
156
|
const draggedElement = draggedClip?.element ?? null;
|
|
157
157
|
const activeDraggedElement =
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { useTimelineEditContextOptional } from "../../contexts/TimelineEditContext";
|
|
3
3
|
import type { TimelineEditCallbacks } from "./timelineCallbacks";
|
|
4
4
|
|
|
5
5
|
// Props a parent (e.g. NLELayout) may pass to <Timeline> to intercept edits —
|
|
@@ -15,7 +15,7 @@ export type TimelineEditOverrides = Pick<
|
|
|
15
15
|
export function useResolvedTimelineEditCallbacks(
|
|
16
16
|
overrides: TimelineEditOverrides,
|
|
17
17
|
): TimelineEditCallbacks {
|
|
18
|
-
const ctx =
|
|
18
|
+
const ctx = useTimelineEditContextOptional();
|
|
19
19
|
const { onMoveElement, onResizeElement, onBlockedEditAttempt, onSplitElement } = overrides;
|
|
20
20
|
return useMemo(
|
|
21
21
|
() => ({
|