@hypit/hypit 0.1.2 → 0.1.4
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/README.md +1 -1
- package/dist/public/studio-adapter.d.ts +31 -2
- package/package.json +4 -1
- package/packages/audio-track-studio/src/index.ts +7 -4
- package/packages/build-result/README.md +58 -0
- package/packages/build-result/package.json +2 -1
- package/packages/build-result/src/decode.ts +10 -8
- package/packages/build-result/src/file-reference.ts +42 -0
- package/packages/build-result/src/index.ts +3 -0
- package/packages/build-result/src/replace-file-windows.ts +58 -0
- package/packages/build-result/src/replace-file.ts +11 -0
- package/packages/build-result/src/store.ts +44 -13
- package/packages/build-result/src/types.ts +38 -8
- package/packages/build-result/src/writer.ts +12 -1
- package/packages/build-result-fs/README.md +23 -1
- package/packages/build-result-s3/README.md +5 -0
- package/packages/build-result-s3/src/repository.ts +19 -5
- package/packages/caption-fine/README.md +17 -2
- package/packages/caption-fine/src/manifest.ts +1 -1
- package/packages/caption-fine/src/render.ts +48 -36
- package/packages/caption-fine-studio/package.json +8 -3
- package/packages/caption-fine-studio/src/index.ts +10 -1
- package/packages/cli/README.md +25 -0
- package/packages/cli/src/arguments.ts +16 -11
- package/packages/cli/src/build-planning.ts +6 -1
- package/packages/cli/src/command.ts +1 -1
- package/packages/cli/src/commands/environment.ts +42 -13
- package/packages/cli/src/machine-view.ts +12 -3
- package/packages/cli/src/main.ts +4 -0
- package/packages/cli/src/output.ts +28 -12
- package/packages/cli/src/result-export.ts +22 -13
- package/packages/cli/src/run-file.ts +19 -8
- package/packages/comment-sticker-studio/src/index.ts +2 -1
- package/packages/compiler-node/src/compiler.ts +1 -0
- package/packages/estimate/README.md +56 -3
- package/packages/estimate/src/program.ts +7 -8
- package/packages/fonts-open/README.md +5 -0
- package/packages/fonts-open/package.json +5 -1
- package/packages/fonts-open/src/studio.ts +21 -0
- package/packages/gpt-image-kits/README.md +17 -12
- package/packages/gpt-image-kits/kits/phone-ugc-v1.svs +8 -8
- package/packages/media-track-studio/src/index.ts +7 -2
- package/packages/provider-whisperx-local/README.md +57 -0
- package/packages/ranking/src/render.ts +11 -9
- package/packages/ranking-studio/README.md +11 -0
- package/packages/ranking-studio/package.json +8 -3
- package/packages/ranking-studio/src/index.ts +11 -4
- package/packages/runtime/src/catalog.ts +2 -0
- package/packages/runtime-host-node/src/index.ts +1 -0
- package/packages/runtime-local/README.md +19 -0
- package/packages/runtime-local/src/programs.ts +39 -24
- package/packages/runtime-local/src/runtime.ts +2 -0
- package/packages/script/README.md +22 -0
- package/packages/script/src/edit.ts +123 -73
- package/packages/script/src/lexical.ts +40 -6
- package/packages/script/src/parser.ts +36 -8
- package/packages/script/src/types.ts +4 -0
- package/packages/seedance-kits/README.md +3 -1
- package/packages/seedance-kits/kits/speaker-v1.svs +5 -5
- package/packages/speech-alignment/src/align.ts +45 -21
- package/packages/studio/INSPECTOR.md +104 -0
- package/packages/studio/README.md +65 -4
- package/packages/studio/src/build-library.ts +99 -84
- package/packages/studio/src/library-media.ts +19 -0
- package/packages/studio/src/parameter-values.ts +41 -1
- package/packages/studio/src/parameters.ts +20 -21
- package/packages/studio/src/server.ts +58 -30
- package/packages/studio/src/shared.ts +20 -6
- package/packages/studio/src/studio-registry.ts +22 -0
- package/packages/studio/src/style.css +135 -144
- package/packages/studio/src/temporal-edit.ts +131 -0
- package/packages/studio/src/ui/artifact-name.ts +80 -0
- package/packages/studio/src/ui/artifact-preview.ts +99 -0
- package/packages/studio/src/ui/icons.ts +5 -1
- package/packages/studio/src/ui/library.ts +373 -192
- package/packages/studio/src/ui/main.ts +153 -31
- package/packages/studio/src/ui/material-preview.ts +1 -1
- package/packages/studio/src/ui/overlay.ts +21 -20
- package/packages/studio/src/ui/sidebar-panel.ts +31 -0
- package/packages/studio/src/ui/stage.ts +124 -8
- package/packages/studio/src/ui/timeline.ts +16 -179
- package/packages/studio/start.ts +15 -3
- package/packages/studio-adapter/README.md +21 -0
- package/packages/studio-adapter/src/index.ts +26 -1
- package/packages/temporal-markup/EDITING.md +183 -0
- package/packages/temporal-markup/README.md +4 -0
- package/packages/typography-track-studio/package.json +8 -3
- package/packages/typography-track-studio/src/index.ts +5 -1
- package/packages/video-cli/README.md +12 -3
- package/packages/video-cli/src/creation.ts +11 -5
- package/packages/whisperx/README.md +7 -0
- package/packages/workspace/src/index.ts +2 -0
- package/packages/workspace-fs-node/README.md +4 -1
- package/packages/workspace-fs-node/src/workspace.ts +3 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { StudioEditHandle, StudioSemanticAnchor, StudioTemporalInstantProjection } from "@hypit/studio-adapter";
|
|
2
|
+
|
|
3
|
+
export type SemanticTarget =
|
|
4
|
+
| { readonly kind: "selection"; readonly startAnchorId: string; readonly endAnchorId: string }
|
|
5
|
+
| { readonly kind: "moment"; readonly anchorId: string };
|
|
6
|
+
|
|
7
|
+
type Span = { readonly startFrame: number; readonly endFrameExclusive: number };
|
|
8
|
+
|
|
9
|
+
/** Project only the semantic write exposed by the handle. This does not invert expressions. */
|
|
10
|
+
export function semanticGestureSpan(
|
|
11
|
+
anchors: readonly StudioSemanticAnchor[], handle: StudioEditHandle, target: SemanticTarget,
|
|
12
|
+
): Span | undefined {
|
|
13
|
+
const semantic = handle.semantic;
|
|
14
|
+
const temporal = handle.temporal;
|
|
15
|
+
if (!semantic || !temporal || target.kind !== semantic.kind) return undefined;
|
|
16
|
+
const byId = new Map(anchors.map((anchor, order) => [anchor.id, { ...anchor, order }]));
|
|
17
|
+
let span: Span;
|
|
18
|
+
if (semantic.kind === "moment" && target.kind === "moment") {
|
|
19
|
+
const previous = byId.get(semantic.anchorId);
|
|
20
|
+
const next = byId.get(target.anchorId);
|
|
21
|
+
if (!previous || !next) return undefined;
|
|
22
|
+
const delta = next.frame - previous.frame;
|
|
23
|
+
span = temporal.kind === "instant"
|
|
24
|
+
? { startFrame: temporal.frame + delta, endFrameExclusive: temporal.frame + delta + 1 }
|
|
25
|
+
: { startFrame: temporal.startFrame + delta, endFrameExclusive: temporal.endFrameExclusive + delta };
|
|
26
|
+
} else if (semantic.kind === "selection" && target.kind === "selection") {
|
|
27
|
+
const start = byId.get(target.startAnchorId);
|
|
28
|
+
const end = byId.get(target.endAnchorId);
|
|
29
|
+
if (!start || !end || start.order > end.order) return undefined;
|
|
30
|
+
if (temporal.kind === "instant") {
|
|
31
|
+
if (temporal.authority.kind !== "semantic") return undefined;
|
|
32
|
+
if (temporal.authority.boundary === "start" && target.endAnchorId !== semantic.endAnchorId
|
|
33
|
+
|| temporal.authority.boundary === "end" && target.startAnchorId !== semantic.startAnchorId) return undefined;
|
|
34
|
+
const frame = temporal.authority.boundary === "start" ? start.frame : end.frame;
|
|
35
|
+
span = { startFrame: frame, endFrameExclusive: frame + 1 };
|
|
36
|
+
} else {
|
|
37
|
+
if (handle.gesture === "trim-start" && target.endAnchorId !== semantic.endAnchorId
|
|
38
|
+
|| handle.gesture === "trim-end" && target.startAnchorId !== semantic.startAnchorId) return undefined;
|
|
39
|
+
if (handle.gesture === "move") {
|
|
40
|
+
const stops = [...new Set(anchors.map((anchor) => anchor.frame))].sort((a, b) => a - b);
|
|
41
|
+
const previousStart = byId.get(semantic.startAnchorId);
|
|
42
|
+
const previousEnd = byId.get(semantic.endAnchorId);
|
|
43
|
+
if (!previousStart || !previousEnd
|
|
44
|
+
|| stops.indexOf(start.frame) - stops.indexOf(previousStart.frame)
|
|
45
|
+
!== stops.indexOf(end.frame) - stops.indexOf(previousEnd.frame)) return undefined;
|
|
46
|
+
}
|
|
47
|
+
span = { startFrame: start.frame, endFrameExclusive: end.frame };
|
|
48
|
+
}
|
|
49
|
+
} else return undefined;
|
|
50
|
+
if (!Number.isSafeInteger(span.startFrame) || span.startFrame < 0
|
|
51
|
+
|| !Number.isSafeInteger(span.endFrameExclusive) || span.endFrameExclusive <= span.startFrame) return undefined;
|
|
52
|
+
if (temporal.kind === "window"
|
|
53
|
+
&& ((handle.gesture === "trim-start" && span.endFrameExclusive !== temporal.endFrameExclusive)
|
|
54
|
+
|| (handle.gesture === "trim-end" && span.startFrame !== temporal.startFrame))) return undefined;
|
|
55
|
+
return span;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A stop is a frame position; coincident anchors remain distinct choices at that stop. */
|
|
59
|
+
export function chooseSemanticGesture(input: {
|
|
60
|
+
readonly anchors: readonly StudioSemanticAnchor[];
|
|
61
|
+
readonly handle: StudioEditHandle;
|
|
62
|
+
readonly pointerStart: number;
|
|
63
|
+
readonly pointerNow: number;
|
|
64
|
+
readonly frameCount: number;
|
|
65
|
+
}): SemanticTarget | undefined {
|
|
66
|
+
const { anchors, handle } = input;
|
|
67
|
+
const semantic = handle.semantic;
|
|
68
|
+
const temporal = handle.temporal;
|
|
69
|
+
if (!semantic || !temporal) return undefined;
|
|
70
|
+
const order = new Map(anchors.map((anchor, index) => [anchor.id, index]));
|
|
71
|
+
const byId = new Map(anchors.map((anchor) => [anchor.id, anchor]));
|
|
72
|
+
const at = new Map<number, StudioSemanticAnchor[]>();
|
|
73
|
+
for (const anchor of anchors) at.set(anchor.frame, [...at.get(anchor.frame) ?? [], anchor]);
|
|
74
|
+
const stops = [...at.keys()].sort((a, b) => a - b);
|
|
75
|
+
const ranked = (choices: readonly StudioSemanticAnchor[], preferred: StudioSemanticAnchor) => [...choices].sort((a, b) =>
|
|
76
|
+
Number(a.id !== preferred.id) - Number(b.id !== preferred.id)
|
|
77
|
+
|| Number(a.kind !== preferred.kind) - Number(b.kind !== preferred.kind)
|
|
78
|
+
|| order.get(a.id)! - order.get(b.id)!);
|
|
79
|
+
const valid = (target: SemanticTarget) => {
|
|
80
|
+
const span = semanticGestureSpan(anchors, handle, target);
|
|
81
|
+
return span !== undefined && (temporal.kind === "instant" ? span.startFrame : span.endFrameExclusive) <= input.frameCount;
|
|
82
|
+
};
|
|
83
|
+
const delta = input.pointerNow - input.pointerStart;
|
|
84
|
+
if (semantic.kind === "moment") {
|
|
85
|
+
const current = byId.get(semantic.anchorId);
|
|
86
|
+
if (!current) return undefined;
|
|
87
|
+
const candidates = ranked(anchors, current).sort((a, b) =>
|
|
88
|
+
Math.abs(a.frame - current.frame - delta) - Math.abs(b.frame - current.frame - delta));
|
|
89
|
+
return candidates.map((anchor): SemanticTarget => ({ kind: "moment", anchorId: anchor.id })).find(valid);
|
|
90
|
+
}
|
|
91
|
+
const start = byId.get(semantic.startAnchorId);
|
|
92
|
+
const end = byId.get(semantic.endAnchorId);
|
|
93
|
+
if (!start || !end) return undefined;
|
|
94
|
+
const target = (a: StudioSemanticAnchor, b: StudioSemanticAnchor): SemanticTarget => ({
|
|
95
|
+
kind: "selection", startAnchorId: a.id, endAnchorId: b.id,
|
|
96
|
+
});
|
|
97
|
+
if (handle.gesture === "move" && temporal.kind === "window") {
|
|
98
|
+
const first = stops.indexOf(start.frame);
|
|
99
|
+
const last = stops.indexOf(end.frame);
|
|
100
|
+
const shifts = stops.map((_, index) => index - first)
|
|
101
|
+
.filter((shift) => last + shift >= 0 && last + shift < stops.length)
|
|
102
|
+
.sort((a, b) => Math.abs(stops[first + a]! - start.frame - delta)
|
|
103
|
+
- Math.abs(stops[first + b]! - start.frame - delta) || Math.abs(a) - Math.abs(b));
|
|
104
|
+
for (const shift of shifts) {
|
|
105
|
+
for (const a of ranked(at.get(stops[first + shift]!)!, start)) {
|
|
106
|
+
for (const b of ranked(at.get(stops[last + shift]!)!, end)) {
|
|
107
|
+
const candidate = target(a, b);
|
|
108
|
+
if (valid(candidate)) return candidate;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
const boundary = temporal.kind === "instant" && temporal.authority.kind === "semantic"
|
|
115
|
+
? temporal.authority.boundary : handle.gesture === "trim-start" ? "start" : "end";
|
|
116
|
+
const current = boundary === "start" ? start : end;
|
|
117
|
+
const candidates = ranked(anchors, current).sort((a, b) =>
|
|
118
|
+
Math.abs(a.frame - current.frame - delta) - Math.abs(b.frame - current.frame - delta));
|
|
119
|
+
return candidates.map((anchor) => boundary === "start" ? target(anchor, end) : target(start, anchor)).find(valid);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
/** Keep the reference and write its local frame offset; a bare reference starts at zero. */
|
|
124
|
+
export function formatTemporalPointEdit(
|
|
125
|
+
reference: StudioTemporalInstantProjection["reference"], desired: number, base?: number,
|
|
126
|
+
): string {
|
|
127
|
+
if (reference === "absolute") return `${desired}f`;
|
|
128
|
+
if (base === undefined) throw new Error(`The ${reference} projection base is unavailable.`);
|
|
129
|
+
const offset = desired - base;
|
|
130
|
+
return `${reference}${offset >= 0 ? "+" : ""}${offset}f`;
|
|
131
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Two-line label with an expanding editor; the caller owns persistence. */
|
|
2
|
+
export function createArtifactName(options: {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly editable: boolean;
|
|
5
|
+
readonly select: () => void;
|
|
6
|
+
readonly save: (name: string) => Promise<string>;
|
|
7
|
+
}) {
|
|
8
|
+
const element = document.createElement("div");
|
|
9
|
+
element.className = "artifact-copy";
|
|
10
|
+
const label = document.createElement("button");
|
|
11
|
+
label.type = "button";
|
|
12
|
+
label.className = "artifact-name";
|
|
13
|
+
let name = options.name;
|
|
14
|
+
const text = document.createElement("span");
|
|
15
|
+
text.className = "artifact-name-text";
|
|
16
|
+
text.textContent = name;
|
|
17
|
+
label.append(text);
|
|
18
|
+
label.title = options.editable ? `${name}\nDouble-click or press F2 to rename` : `${name}\nNaming is available when this Build finishes`;
|
|
19
|
+
element.append(label);
|
|
20
|
+
label.addEventListener("click", (event) => { event.stopPropagation(); options.select(); });
|
|
21
|
+
let editing = false;
|
|
22
|
+
|
|
23
|
+
function edit(): void {
|
|
24
|
+
if (editing || !options.editable) return;
|
|
25
|
+
options.select();
|
|
26
|
+
editing = true;
|
|
27
|
+
element.classList.add("editing");
|
|
28
|
+
label.hidden = true;
|
|
29
|
+
const field = document.createElement("textarea");
|
|
30
|
+
field.className = "artifact-name-editor";
|
|
31
|
+
field.value = name;
|
|
32
|
+
field.rows = 1;
|
|
33
|
+
field.setAttribute("aria-label", "Display name");
|
|
34
|
+
const error = document.createElement("span");
|
|
35
|
+
error.className = "artifact-name-error";
|
|
36
|
+
error.setAttribute("role", "status");
|
|
37
|
+
element.append(field, error);
|
|
38
|
+
const resize = () => { field.style.height = "0px"; field.style.height = `${field.scrollHeight + field.offsetHeight - field.clientHeight}px`; };
|
|
39
|
+
resize();
|
|
40
|
+
field.focus();
|
|
41
|
+
field.setSelectionRange(name.length, name.length);
|
|
42
|
+
field.addEventListener("input", resize);
|
|
43
|
+
field.addEventListener("click", (event) => event.stopPropagation());
|
|
44
|
+
let saving = false;
|
|
45
|
+
const close = () => {
|
|
46
|
+
editing = false;
|
|
47
|
+
field.remove();
|
|
48
|
+
error.remove();
|
|
49
|
+
label.hidden = false;
|
|
50
|
+
element.classList.remove("editing");
|
|
51
|
+
};
|
|
52
|
+
const save = async () => {
|
|
53
|
+
if (!editing || saving) return;
|
|
54
|
+
const value = field.value.trim();
|
|
55
|
+
if (value === name) { close(); return; }
|
|
56
|
+
if (value.length === 0) { error.textContent = "Enter a name."; return; }
|
|
57
|
+
saving = true;
|
|
58
|
+
field.readOnly = true;
|
|
59
|
+
error.textContent = "Saving…";
|
|
60
|
+
try {
|
|
61
|
+
name = await options.save(value);
|
|
62
|
+
text.textContent = name;
|
|
63
|
+
label.title = `${name}\nDouble-click or press F2 to rename`;
|
|
64
|
+
close();
|
|
65
|
+
} catch (cause) {
|
|
66
|
+
error.textContent = cause instanceof Error ? cause.message : String(cause);
|
|
67
|
+
} finally {
|
|
68
|
+
saving = false;
|
|
69
|
+
field.readOnly = false;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
field.addEventListener("blur", () => { void save(); });
|
|
73
|
+
field.addEventListener("keydown", (event) => {
|
|
74
|
+
if (event.key === "Escape" && !saving) { event.preventDefault(); event.stopPropagation(); close(); label.focus(); }
|
|
75
|
+
else if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); void save(); }
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
label.addEventListener("dblclick", (event) => { event.preventDefault(); event.stopPropagation(); edit(); });
|
|
79
|
+
return { element, edit };
|
|
80
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { StudioArtifactView } from "../shared.js";
|
|
2
|
+
import { audioPreview } from "./material-preview.js";
|
|
3
|
+
|
|
4
|
+
/** A media viewer only: it does not change the composition or its playhead. */
|
|
5
|
+
export function createArtifactPreview(changed: () => void) {
|
|
6
|
+
const element = document.createElement("div");
|
|
7
|
+
element.className = "stage-artifact";
|
|
8
|
+
element.hidden = true;
|
|
9
|
+
let selected: StudioArtifactView | undefined;
|
|
10
|
+
let media: HTMLMediaElement | undefined;
|
|
11
|
+
let generation = 0;
|
|
12
|
+
|
|
13
|
+
function close(): void {
|
|
14
|
+
generation += 1;
|
|
15
|
+
selected = undefined;
|
|
16
|
+
if (media !== undefined) {
|
|
17
|
+
media.pause();
|
|
18
|
+
media.removeAttribute("src");
|
|
19
|
+
media.load();
|
|
20
|
+
}
|
|
21
|
+
media = undefined;
|
|
22
|
+
element.replaceChildren();
|
|
23
|
+
element.hidden = true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function open(artifact: StudioArtifactView, muted: boolean): void {
|
|
27
|
+
close();
|
|
28
|
+
const current = generation;
|
|
29
|
+
selected = artifact;
|
|
30
|
+
element.hidden = false;
|
|
31
|
+
const url = `/__studio/artifact?${new URLSearchParams({ build: artifact.build, output: artifact.output })}`;
|
|
32
|
+
const error = document.createElement("p");
|
|
33
|
+
error.className = "stage-media-error";
|
|
34
|
+
error.hidden = true;
|
|
35
|
+
error.setAttribute("role", "status");
|
|
36
|
+
const failed = () => {
|
|
37
|
+
if (generation !== current) return;
|
|
38
|
+
error.textContent = "This media could not be opened in the browser.";
|
|
39
|
+
error.hidden = false;
|
|
40
|
+
changed();
|
|
41
|
+
};
|
|
42
|
+
if (artifact.mediaType.startsWith("image/")) {
|
|
43
|
+
const image = document.createElement("img");
|
|
44
|
+
image.alt = artifact.displayName ?? artifact.output;
|
|
45
|
+
image.addEventListener("error", failed);
|
|
46
|
+
image.src = url;
|
|
47
|
+
element.append(image);
|
|
48
|
+
} else {
|
|
49
|
+
const audio = artifact.mediaType.startsWith("audio/");
|
|
50
|
+
const player = document.createElement(audio ? "audio" : "video");
|
|
51
|
+
media = player;
|
|
52
|
+
if (player instanceof HTMLVideoElement) player.playsInline = true;
|
|
53
|
+
player.preload = "metadata";
|
|
54
|
+
player.muted = muted;
|
|
55
|
+
for (const event of ["loadedmetadata", "durationchange", "timeupdate", "play", "pause", "ended", "seeking", "seeked"]) {
|
|
56
|
+
player.addEventListener(event, () => { if (generation === current) changed(); });
|
|
57
|
+
}
|
|
58
|
+
player.addEventListener("error", failed);
|
|
59
|
+
player.src = url;
|
|
60
|
+
element.append(player);
|
|
61
|
+
if (audio) {
|
|
62
|
+
const wave = document.createElement("div");
|
|
63
|
+
wave.className = "stage-audio-wave";
|
|
64
|
+
wave.textContent = "Audio";
|
|
65
|
+
element.append(wave);
|
|
66
|
+
void audioPreview(url).then((waveform) => {
|
|
67
|
+
if (generation !== current || waveform === undefined) return;
|
|
68
|
+
const image = document.createElement("img");
|
|
69
|
+
image.src = waveform;
|
|
70
|
+
image.alt = "Audio waveform";
|
|
71
|
+
wave.replaceChildren(image);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
element.append(error);
|
|
76
|
+
changed();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function seek(seconds: number): void {
|
|
80
|
+
if (media === undefined || !Number.isFinite(media.duration)) return;
|
|
81
|
+
media.currentTime = Math.max(0, Math.min(media.duration, seconds));
|
|
82
|
+
changed();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function toggle(): void {
|
|
86
|
+
const player = media;
|
|
87
|
+
if (player === undefined) return;
|
|
88
|
+
if (!player.paused) player.pause();
|
|
89
|
+
else {
|
|
90
|
+
if (player.ended) player.currentTime = 0;
|
|
91
|
+
void player.play().catch(() => changed());
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { element, open, close, seek, toggle,
|
|
96
|
+
get selected() { return selected; },
|
|
97
|
+
get media() { return media; },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -14,7 +14,6 @@ const paths: Readonly<Record<string, string>> = {
|
|
|
14
14
|
results: '<path d="M4 8v12h16V8M3 4h18v4H3zM9 12h6"/>',
|
|
15
15
|
run: '<circle cx="12" cy="12" r="9"/><path d="m10 8 6 4-6 4V8Z"/>',
|
|
16
16
|
image: '<rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="8.5" cy="9" r="2"/><path d="m4 18 5-5 3 3 3-4 5 6"/>',
|
|
17
|
-
sidebarCollapse: '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M8 4v16m7-11-3 3 3 3"/>',
|
|
18
17
|
wrap: '<path d="M4 6h16M4 10h12a4 4 0 0 1 0 8h-5m3-3-3 3 3 3M4 14h5M4 18h4"/>',
|
|
19
18
|
timeline: '<path d="M3 6h18M3 12h18M3 18h18"/><path d="M8 4v4M16 10v4M11 16v4"/>',
|
|
20
19
|
minus: '<path d="M5 12h14"/>',
|
|
@@ -40,6 +39,11 @@ const paths: Readonly<Record<string, string>> = {
|
|
|
40
39
|
layers: '<path d="m12 3 9 5-9 5-9-5 9-5Z"/><path d="m3 12 9 5 9-5M3 16l9 5 9-5"/>',
|
|
41
40
|
ranking: '<path d="M5 19V9h4v10M10 19V5h4v14M15 19v-7h4v7M3 19h18"/>',
|
|
42
41
|
refresh: '<path d="M20 7v5h-5M4 17v-5h5"/><path d="M18.5 9a7 7 0 0 0-11.8-2.3L4 9M5.5 15a7 7 0 0 0 11.8 2.3L20 15"/>',
|
|
42
|
+
check: '<circle cx="12" cy="12" r="9"/><path d="m8 12 3 3 5-6"/>',
|
|
43
|
+
alert: '<circle cx="12" cy="12" r="9"/><path d="M12 7v6M12 17h.01"/>',
|
|
44
|
+
arrowRight: '<path d="M5 12h14m-6-6 6 6-6 6"/>',
|
|
45
|
+
close: '<path d="m6 6 12 12M18 6 6 18"/>',
|
|
46
|
+
panel: '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M8 4v16M8 9h13"/>',
|
|
43
47
|
};
|
|
44
48
|
|
|
45
49
|
export function icon(name: string, className = "icon"): string {
|