@hypit/hypit 0.1.3 → 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/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
|
@@ -6,7 +6,11 @@ import type {
|
|
|
6
6
|
StudioTaskView,
|
|
7
7
|
} from "../shared.js";
|
|
8
8
|
import type { CodePane } from "./code.js";
|
|
9
|
+
import { createArtifactName } from "./artifact-name.js";
|
|
10
|
+
import { audioPreview } from "./material-preview.js";
|
|
11
|
+
import { createSidebarPanel, sidebarItem } from "./sidebar-panel.js";
|
|
9
12
|
import { icon } from "./icons.js";
|
|
13
|
+
import { mergeStudioArtifacts } from "../library-media.js";
|
|
10
14
|
|
|
11
15
|
type LibrarySection = "source" | "tasks" | "artifacts";
|
|
12
16
|
|
|
@@ -14,6 +18,7 @@ export type LibraryPane = {
|
|
|
14
18
|
readonly element: HTMLElement;
|
|
15
19
|
show(snapshot: StudioSnapshot): void;
|
|
16
20
|
refresh(): Promise<void>;
|
|
21
|
+
selectArtifact(id: string | undefined): void;
|
|
17
22
|
};
|
|
18
23
|
|
|
19
24
|
function leaf(path: string): string {
|
|
@@ -26,10 +31,6 @@ function stem(path: string): string {
|
|
|
26
31
|
return dot > 0 ? name.slice(0, dot) : name;
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
function compactId(value: string): string {
|
|
30
|
-
return value.length <= 18 ? value : `${value.slice(0, 9)}…${value.slice(-6)}`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
34
|
function formatBytes(value: number): string {
|
|
34
35
|
if (value < 1_000) return `${value} B`;
|
|
35
36
|
if (value < 1_000_000) return `${(value / 1_000).toFixed(value < 10_000 ? 1 : 0)} KB`;
|
|
@@ -55,7 +56,8 @@ function emptyState(iconName: string, title: string, detail: string): HTMLElemen
|
|
|
55
56
|
|
|
56
57
|
function taskProgress(task: StudioTaskView): string | undefined {
|
|
57
58
|
const progress = task.operations.find((operation) => operation.status === "pending" && operation.phase !== undefined);
|
|
58
|
-
if (progress === undefined) return undefined
|
|
59
|
+
if (progress === undefined) return task.requests === undefined ? undefined
|
|
60
|
+
: `${task.requests.completed}/${task.requests.total} steps complete`;
|
|
59
61
|
if (progress.completed === undefined) return progress.phase;
|
|
60
62
|
const amount = progress.total === undefined
|
|
61
63
|
? String(progress.completed)
|
|
@@ -63,33 +65,60 @@ function taskProgress(task: StudioTaskView): string | undefined {
|
|
|
63
65
|
return `${progress.phase} · ${amount}${progress.unit === undefined ? "" : ` ${progress.unit}`}`;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
function taskCard(task: StudioTaskView): HTMLElement {
|
|
68
|
+
function taskCard(task: StudioTaskView, selected: boolean, select: () => void, showMedia: () => void): HTMLElement {
|
|
67
69
|
const node = document.createElement("article");
|
|
68
|
-
node.className = `task-card task-${task.status}`;
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
node.className = `task-card task-${task.status}${selected ? " selected" : ""}`;
|
|
71
|
+
node.tabIndex = 0;
|
|
72
|
+
node.setAttribute("aria-label", task.title ?? stem(task.run ?? task.source));
|
|
73
|
+
node.addEventListener("click", select);
|
|
74
|
+
node.addEventListener("keydown", (event) => {
|
|
75
|
+
if (event.target === node && (event.key === "Enter" || event.key === " ")) { event.preventDefault(); select(); }
|
|
76
|
+
});
|
|
77
|
+
node.dataset.taskId = task.id;
|
|
78
|
+
const source = task.run ?? task.source;
|
|
79
|
+
const title = task.title ?? stem(source);
|
|
80
|
+
const statuses: Record<StudioTaskView["status"], string> = {
|
|
81
|
+
queued: "Queued", running: "Running", waiting: "Waiting", complete: "Completed",
|
|
82
|
+
failed: "Failed", cancelled: "Cancelled", "saving-result": "Saving", attention: "Needs attention",
|
|
83
|
+
};
|
|
71
84
|
node.innerHTML = `
|
|
72
|
-
<div class="task-state"
|
|
85
|
+
<div class="task-state" aria-hidden="true">${icon(task.ongoing ? "when" : task.status === "complete" ? "check" : task.status === "failed" ? "alert" : "tasks")}</div>
|
|
73
86
|
<div class="task-copy">
|
|
74
|
-
<div class="task-title"><
|
|
75
|
-
<
|
|
76
|
-
<div class="task-facts"><span data-time></span><span data-records></span><span data-command></span></div>
|
|
87
|
+
<div class="task-title"><button type="button" class="task-name"></button></div>
|
|
88
|
+
<div class="task-facts"><span class="task-status"></span><time></time></div>
|
|
77
89
|
<div class="task-progress" data-progress></div>
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
node.querySelector<HTMLElement>("
|
|
85
|
-
node.querySelector
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
<div class="task-detail" data-detail></div>
|
|
91
|
+
</div>
|
|
92
|
+
<button type="button" class="task-open" aria-label="View task media" title="View media">${icon("arrowRight")}</button>`;
|
|
93
|
+
const name = node.querySelector<HTMLButtonElement>(".task-name")!;
|
|
94
|
+
name.textContent = title;
|
|
95
|
+
name.addEventListener("click", (event) => { event.stopPropagation(); select(); });
|
|
96
|
+
node.querySelector<HTMLElement>(".task-status")!.textContent = statuses[task.status];
|
|
97
|
+
const time = node.querySelector("time")!;
|
|
98
|
+
time.dateTime = new Date(task.createdAt).toISOString();
|
|
99
|
+
time.textContent = date.format(task.createdAt);
|
|
100
|
+
const progress = task.ongoing ? taskProgress(task) : undefined;
|
|
89
101
|
const progressNode = node.querySelector<HTMLElement>("[data-progress]")!;
|
|
90
102
|
if (progress === undefined) progressNode.remove();
|
|
91
|
-
else progressNode.textContent = progress;
|
|
92
|
-
|
|
103
|
+
else { progressNode.textContent = progress; progressNode.title = progress; }
|
|
104
|
+
const detailNode = node.querySelector<HTMLElement>("[data-detail]")!;
|
|
105
|
+
if (task.detail === undefined) detailNode.remove();
|
|
106
|
+
else {
|
|
107
|
+
const message = document.createElement("div");
|
|
108
|
+
message.className = "task-error-message";
|
|
109
|
+
message.textContent = task.detail;
|
|
110
|
+
detailNode.append(message);
|
|
111
|
+
const copy = document.createElement("button");
|
|
112
|
+
copy.type = "button"; copy.className = "task-copy-error"; copy.textContent = "Copy error";
|
|
113
|
+
copy.addEventListener("click", (event) => {
|
|
114
|
+
event.stopPropagation();
|
|
115
|
+
void navigator.clipboard.writeText(task.detail!).then(() => { copy.textContent = "Copied"; }, () => { copy.textContent = "Select the text to copy"; });
|
|
116
|
+
});
|
|
117
|
+
detailNode.append(copy);
|
|
118
|
+
detailNode.addEventListener("click", (event) => { event.stopPropagation(); if (!node.classList.contains("selected")) select(); });
|
|
119
|
+
}
|
|
120
|
+
node.querySelector<HTMLButtonElement>(".task-open")!.addEventListener("click", (event) => { event.stopPropagation(); showMedia(); });
|
|
121
|
+
name.title = [title, source, task.id, task.note].filter((item) => item !== undefined).join("\n");
|
|
93
122
|
return node;
|
|
94
123
|
}
|
|
95
124
|
|
|
@@ -100,63 +129,115 @@ function artifactKind(mediaType: string): { readonly label: string; readonly ico
|
|
|
100
129
|
return { label: "Data", icon: "code" };
|
|
101
130
|
}
|
|
102
131
|
|
|
103
|
-
function
|
|
132
|
+
function sizeThumbnail(media: HTMLElement, width: number, height: number): void {
|
|
133
|
+
if (width <= 0 || height <= 0) return;
|
|
134
|
+
const longest = Math.max(width, height);
|
|
135
|
+
media.style.width = `${100 * width / longest}%`;
|
|
136
|
+
media.style.height = `${100 * height / longest}%`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function artifactCard(artifact: StudioArtifactView, open: (artifact: StudioArtifactView) => void, renamed: (artifact: StudioArtifactView) => void): HTMLElement {
|
|
104
140
|
const kind = artifactKind(artifact.mediaType);
|
|
105
|
-
const link = document.createElement("
|
|
141
|
+
const link = document.createElement("div");
|
|
142
|
+
link.tabIndex = 0;
|
|
106
143
|
link.className = `artifact-card artifact-${kind.label.toLowerCase()}${artifact.highlighted ? " artifact-highlighted" : ""}`;
|
|
107
144
|
const query = new URLSearchParams({
|
|
108
145
|
build: artifact.build,
|
|
109
146
|
output: artifact.output,
|
|
110
|
-
path: artifact.valuePath,
|
|
111
147
|
});
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
link.
|
|
115
|
-
|
|
148
|
+
const href = `/__studio/artifact?${query.toString()}`;
|
|
149
|
+
|
|
150
|
+
link.dataset.artifactId = artifact.id;
|
|
151
|
+
link.dataset.build = artifact.build;
|
|
152
|
+
link.dataset.output = artifact.output;
|
|
153
|
+
link.dataset.displayName = artifact.displayName ?? "";
|
|
154
|
+
link.dataset.nameEditable = String(artifact.nameEditable === true);
|
|
155
|
+
const title = artifact.displayName ?? artifact.output;
|
|
116
156
|
link.innerHTML = `
|
|
117
157
|
<div class="artifact-preview">
|
|
118
158
|
<span class="artifact-glyph">${icon(kind.icon)}</span>
|
|
119
159
|
<span class="artifact-type"></span>
|
|
120
|
-
${artifact.highlighted ? '<span class="artifact-highlight">Highlighted</span>' : ""}
|
|
121
|
-
</div>
|
|
122
|
-
<div class="artifact-copy">
|
|
123
|
-
<strong></strong>
|
|
124
|
-
<span class="artifact-meta"></span>
|
|
125
160
|
</div>`;
|
|
126
161
|
link.querySelector<HTMLElement>(".artifact-type")!.textContent = kind.label;
|
|
127
|
-
link.querySelector("
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
162
|
+
const preview = link.querySelector<HTMLElement>(".artifact-preview")!;
|
|
163
|
+
preview.setAttribute("role", "button");
|
|
164
|
+
preview.setAttribute("aria-label", `Preview ${title}`);
|
|
165
|
+
preview.addEventListener("click", () => open(artifact));
|
|
166
|
+
const name = createArtifactName({
|
|
167
|
+
name: title,
|
|
168
|
+
editable: artifact.nameEditable === true,
|
|
169
|
+
select: () => { if (!link.classList.contains("selected")) open(artifact); },
|
|
170
|
+
async save(displayName) {
|
|
171
|
+
const response = await fetch("/__studio/artifact-name", {
|
|
172
|
+
method: "PUT", headers: { "Content-Type": "application/json" },
|
|
173
|
+
body: JSON.stringify({ build: artifact.build, output: artifact.output, displayName }),
|
|
174
|
+
});
|
|
175
|
+
const result = await response.json() as { displayName?: string; error?: string };
|
|
176
|
+
if (!response.ok || typeof result.displayName !== "string") throw new Error(result.error ?? "Could not save the name");
|
|
177
|
+
artifact = { ...artifact, displayName: result.displayName };
|
|
178
|
+
link.dataset.displayName = result.displayName;
|
|
179
|
+
renamed(artifact);
|
|
180
|
+
preview.setAttribute("aria-label", `Preview ${result.displayName}`);
|
|
181
|
+
preview.title = [result.displayName, ...preview.title.split("\n").slice(1)].join("\n");
|
|
182
|
+
return result.displayName;
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
link.append(name.element);
|
|
186
|
+
link.addEventListener("keydown", (event) => {
|
|
187
|
+
if (event.target instanceof HTMLTextAreaElement) return;
|
|
188
|
+
if (event.key === "F2") { event.preventDefault(); name.edit(); }
|
|
189
|
+
else if (event.key === "Enter" && event.target === link) { event.preventDefault(); open(artifact); }
|
|
190
|
+
});
|
|
133
191
|
if (artifact.mediaType.startsWith("image/")) {
|
|
134
192
|
const image = document.createElement("img");
|
|
135
|
-
image.
|
|
193
|
+
image.addEventListener("load", () => sizeThumbnail(image, image.naturalWidth, image.naturalHeight));
|
|
194
|
+
image.addEventListener("error", () => { link.classList.add("preview-unavailable"); link.querySelector<HTMLElement>(".artifact-type")!.textContent = "Preview unavailable"; });
|
|
195
|
+
image.src = href;
|
|
136
196
|
image.alt = "";
|
|
137
197
|
image.loading = "lazy";
|
|
138
198
|
link.querySelector<HTMLElement>(".artifact-preview")!.prepend(image);
|
|
199
|
+
} else if (artifact.mediaType.startsWith("video/")) {
|
|
200
|
+
const video = document.createElement("video");
|
|
201
|
+
video.preload = "none";
|
|
202
|
+
video.muted = true;
|
|
203
|
+
video.playsInline = true;
|
|
204
|
+
video.dataset.previewSource = href;
|
|
205
|
+
video.setAttribute("aria-hidden", "true");
|
|
206
|
+
const label = link.querySelector<HTMLElement>(".artifact-type")!;
|
|
207
|
+
label.textContent = "Video · Loading preview";
|
|
208
|
+
const ready = () => {
|
|
209
|
+
if (video.readyState < 2 || video.seeking) return;
|
|
210
|
+
video.classList.add("is-ready");
|
|
211
|
+
label.textContent = "Video";
|
|
212
|
+
};
|
|
213
|
+
video.addEventListener("loadedmetadata", () => {
|
|
214
|
+
sizeThumbnail(video, video.videoWidth, video.videoHeight);
|
|
215
|
+
video.currentTime = Number.isFinite(video.duration) ? Math.min(0.1, video.duration / 2) : 0.1;
|
|
216
|
+
}, { once: true });
|
|
217
|
+
video.addEventListener("loadeddata", ready);
|
|
218
|
+
video.addEventListener("seeked", ready);
|
|
219
|
+
video.addEventListener("error", () => { label.textContent = "Preview unavailable"; link.classList.add("preview-unavailable"); });
|
|
220
|
+
link.querySelector<HTMLElement>(".artifact-preview")!.prepend(video);
|
|
221
|
+
} else if (artifact.mediaType.startsWith("audio/")) {
|
|
222
|
+
const waveform = document.createElement("img");
|
|
223
|
+
waveform.className = "artifact-waveform";
|
|
224
|
+
waveform.alt = "";
|
|
225
|
+
waveform.dataset.audioSource = href;
|
|
226
|
+
link.querySelector<HTMLElement>(".artifact-preview")!.prepend(waveform);
|
|
139
227
|
}
|
|
140
|
-
|
|
228
|
+
preview.title = [
|
|
141
229
|
title,
|
|
142
230
|
artifact.mediaType,
|
|
143
|
-
|
|
144
|
-
|
|
231
|
+
formatBytes(artifact.size),
|
|
232
|
+
date.format(artifact.createdAt),
|
|
233
|
+
artifact.ownerBuild === undefined ? artifact.filePath : `${artifact.ownerBuild}/${artifact.filePath}`,
|
|
234
|
+
...artifact.origins.map((origin) => `${origin.run ?? origin.source} · ${origin.build} · ${origin.output}`),
|
|
145
235
|
artifact.buildNote,
|
|
146
236
|
].filter((item) => item !== undefined).join("\n");
|
|
147
237
|
return link;
|
|
148
238
|
}
|
|
149
239
|
|
|
150
|
-
function
|
|
151
|
-
const button = document.createElement("button");
|
|
152
|
-
button.type = "button";
|
|
153
|
-
button.className = "library-older";
|
|
154
|
-
button.textContent = "Load older";
|
|
155
|
-
button.addEventListener("click", load);
|
|
156
|
-
return button;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function createLibraryPane(code: CodePane): LibraryPane {
|
|
240
|
+
export function createLibraryPane(code: CodePane, openArtifact: (artifact: StudioArtifactView) => void, renamedArtifact: (artifact: StudioArtifactView) => void): LibraryPane {
|
|
160
241
|
const element = document.createElement("section");
|
|
161
242
|
element.className = "library";
|
|
162
243
|
element.innerHTML = `
|
|
@@ -172,46 +253,83 @@ export function createLibraryPane(code: CodePane): LibraryPane {
|
|
|
172
253
|
</button>
|
|
173
254
|
</div>
|
|
174
255
|
<section class="library-view active" data-library-view="source">
|
|
175
|
-
<div class="source-workspace" data-source-workspace>
|
|
176
|
-
<div class="source-sidebar-heading">
|
|
177
|
-
<span class="source-sidebar-mark" aria-hidden="true">${icon("sidebarCollapse")}</span>
|
|
178
|
-
</div>
|
|
179
|
-
<div class="source-code-heading" data-source-toolbar></div>
|
|
180
|
-
<nav class="source-files" aria-label="Referenced source files" data-source-files></nav>
|
|
181
|
-
<div class="source-code" data-source-code></div>
|
|
182
|
-
</div>
|
|
183
|
-
</section>
|
|
184
|
-
<section class="library-view" data-library-view="tasks">
|
|
185
|
-
<div class="library-toolbar">
|
|
186
|
-
<div><strong>Build Results</strong><small data-task-count></small></div>
|
|
187
|
-
<button type="button" class="library-refresh" data-library-refresh aria-label="Refresh tasks" title="Refresh">${icon("refresh")}</button>
|
|
188
|
-
</div>
|
|
189
|
-
<div class="library-context-row" data-runtime-context></div>
|
|
190
|
-
<div class="task-list" data-task-list></div>
|
|
191
256
|
</section>
|
|
257
|
+
<section class="library-view" data-library-view="tasks"></section>
|
|
192
258
|
<section class="library-view" data-library-view="artifacts">
|
|
193
|
-
<div class="library-toolbar">
|
|
194
|
-
<div><strong>Accepted artifacts</strong><small data-artifact-count></small></div>
|
|
195
|
-
<button type="button" class="library-refresh" data-library-refresh aria-label="Refresh artifacts" title="Refresh">${icon("refresh")}</button>
|
|
196
|
-
</div>
|
|
197
|
-
<div class="artifact-filters" role="tablist" aria-label="Artifact kinds" data-artifact-filters></div>
|
|
198
|
-
<div class="artifact-grid" data-artifact-grid></div>
|
|
199
259
|
</section>`;
|
|
200
260
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
261
|
+
const sourcePanel = createSidebarPanel("panel", "Referenced source files");
|
|
262
|
+
sourcePanel.toolbar.append(code.toolbar);
|
|
263
|
+
sourcePanel.content.classList.add("source-code");
|
|
264
|
+
sourcePanel.content.append(code.element);
|
|
265
|
+
element.querySelector('[data-library-view="source"]')!.append(sourcePanel.element);
|
|
266
|
+
const taskPanel = createSidebarPanel("panel", "Task categories");
|
|
267
|
+
taskPanel.toolbar.innerHTML = `<span class="code-location" data-task-heading>All tasks</span><div class="task-context" data-task-context hidden></div>
|
|
268
|
+
<button type="button" class="library-refresh" data-library-refresh aria-label="Refresh tasks" title="Refresh">${icon("refresh")}</button>`;
|
|
269
|
+
taskPanel.content.innerHTML = `<div class="media-status" data-library-status="tasks" role="status" hidden></div><div class="task-list" data-task-list></div>`;
|
|
270
|
+
element.querySelector('[data-library-view="tasks"]')!.append(taskPanel.element);
|
|
271
|
+
const mediaPanel = createSidebarPanel("panel", "Media categories");
|
|
272
|
+
mediaPanel.toolbar.innerHTML = `<span class="code-location" data-media-heading>All media</span><div class="task-context" data-task-context hidden></div>
|
|
273
|
+
<button type="button" class="library-refresh" data-library-refresh aria-label="Refresh media" title="Refresh">${icon("refresh")}</button>`;
|
|
274
|
+
mediaPanel.content.innerHTML = `<div class="media-status" data-library-status="artifacts" role="status" hidden></div><div class="artifact-grid" data-artifact-grid></div>`;
|
|
275
|
+
element.querySelector('[data-library-view="artifacts"]')!.append(mediaPanel.element);
|
|
276
|
+
const sourceList = sourcePanel.navigation;
|
|
204
277
|
const taskList = element.querySelector<HTMLElement>("[data-task-list]")!;
|
|
205
278
|
const artifactGrid = element.querySelector<HTMLElement>("[data-artifact-grid]")!;
|
|
206
|
-
const artifactFilters =
|
|
207
|
-
const runtimeContext = element.querySelector<HTMLElement>("[data-runtime-context]")!;
|
|
279
|
+
const artifactFilters = mediaPanel.navigation;
|
|
208
280
|
let snapshot: StudioSnapshot | undefined;
|
|
209
281
|
let selectedSource = "";
|
|
210
282
|
let active: LibrarySection = "source";
|
|
211
|
-
|
|
283
|
+
const libraries: Partial<Record<"tasks" | "artifacts", StudioLibraryView>> = {};
|
|
284
|
+
let taskFilter: "all" | "ongoing" | "finished" = "all";
|
|
212
285
|
let artifactFilter = "all";
|
|
213
|
-
let
|
|
214
|
-
let
|
|
286
|
+
let selectedArtifact: string | undefined;
|
|
287
|
+
let selectedTask: StudioTaskView | undefined;
|
|
288
|
+
let request: AbortController | undefined;
|
|
289
|
+
const morePages = new IntersectionObserver((entries) => {
|
|
290
|
+
for (const entry of entries) {
|
|
291
|
+
const node = entry.target as HTMLElement;
|
|
292
|
+
const section = node.dataset.moreSection as "tasks" | "artifacts";
|
|
293
|
+
if (!entry.isIntersecting || request !== undefined || active !== section) continue;
|
|
294
|
+
morePages.unobserve(node);
|
|
295
|
+
void refresh(section, node.dataset.before);
|
|
296
|
+
}
|
|
297
|
+
}, { rootMargin: "0px 0px 120px 0px" });
|
|
298
|
+
const continuation = (section: "tasks" | "artifacts", cursor: string): HTMLElement => {
|
|
299
|
+
const node = document.createElement("div");
|
|
300
|
+
node.className = "library-continuation";
|
|
301
|
+
node.dataset.moreSection = section;
|
|
302
|
+
node.dataset.before = cursor;
|
|
303
|
+
node.textContent = "Loading more…";
|
|
304
|
+
return node;
|
|
305
|
+
};
|
|
306
|
+
const selectArtifact = (id: string | undefined): void => {
|
|
307
|
+
selectedArtifact = id;
|
|
308
|
+
for (const card of Array.from(artifactGrid.querySelectorAll<HTMLElement>("[data-artifact-id]"))) {
|
|
309
|
+
const selected = card.dataset.artifactId === id;
|
|
310
|
+
card.classList.toggle("selected", selected);
|
|
311
|
+
if (selected) card.setAttribute("aria-current", "true");
|
|
312
|
+
else card.removeAttribute("aria-current");
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
const videoPreviews = new IntersectionObserver((entries) => {
|
|
316
|
+
for (const entry of entries) {
|
|
317
|
+
if (!entry.isIntersecting) continue;
|
|
318
|
+
videoPreviews.unobserve(entry.target);
|
|
319
|
+
if (entry.target instanceof HTMLImageElement) {
|
|
320
|
+
const image = entry.target;
|
|
321
|
+
void audioPreview(image.dataset.audioSource!).then((url) => {
|
|
322
|
+
if (url === undefined || !image.isConnected) return;
|
|
323
|
+
image.src = url;
|
|
324
|
+
image.classList.add("is-ready");
|
|
325
|
+
});
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const video = entry.target as HTMLVideoElement;
|
|
329
|
+
video.preload = "metadata";
|
|
330
|
+
video.src = video.dataset.previewSource!;
|
|
331
|
+
}
|
|
332
|
+
}, { root: artifactGrid });
|
|
215
333
|
|
|
216
334
|
const renderSources = (): void => {
|
|
217
335
|
if (snapshot === undefined) return;
|
|
@@ -224,75 +342,140 @@ export function createLibraryPane(code: CodePane): LibraryPane {
|
|
|
224
342
|
selectedSource = code.activePath() ?? snapshot.source.path;
|
|
225
343
|
}
|
|
226
344
|
sourceList.replaceChildren(...files.map((file) => {
|
|
227
|
-
const button =
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
button.title = `${file.role} · ${file.path}${file.imports.length === 0 ? "" : `\nimports ${file.imports.join(", ")}`}`;
|
|
233
|
-
button.addEventListener("click", () => {
|
|
234
|
-
selectedSource = file.path;
|
|
235
|
-
renderSources();
|
|
345
|
+
const button = sidebarItem({
|
|
346
|
+
label: `${stem(file.path)} ${file.language.toUpperCase()}`,
|
|
347
|
+
icon: file.language === "svrun" ? "run" : file.language === "svs" ? "tune" : "code",
|
|
348
|
+
selected: file.path === selectedSource,
|
|
349
|
+
select: () => { selectedSource = file.path; renderSources(); },
|
|
236
350
|
});
|
|
351
|
+
button.title = `${file.role} · ${file.path}${file.imports.length === 0 ? "" : `\nimports ${file.imports.join(", ")}`}`;
|
|
237
352
|
return button;
|
|
238
353
|
}));
|
|
239
354
|
};
|
|
240
355
|
|
|
356
|
+
const renderTaskContext = (): void => {
|
|
357
|
+
for (const node of Array.from(element.querySelectorAll<HTMLElement>("[data-task-context]"))) {
|
|
358
|
+
const title = selectedTask === undefined ? undefined : selectedTask.title ?? stem(selectedTask.run ?? selectedTask.source);
|
|
359
|
+
if (selectedTask !== undefined && node.dataset.taskId === selectedTask.id
|
|
360
|
+
&& node.querySelector(".task-context-name span")?.textContent === title) continue;
|
|
361
|
+
node.replaceChildren();
|
|
362
|
+
node.hidden = selectedTask === undefined;
|
|
363
|
+
node.dataset.taskId = selectedTask?.id ?? "";
|
|
364
|
+
if (selectedTask === undefined) continue;
|
|
365
|
+
const task = selectedTask;
|
|
366
|
+
const back = document.createElement("button");
|
|
367
|
+
back.type = "button"; back.className = "task-context-name";
|
|
368
|
+
back.innerHTML = "<span></span>";
|
|
369
|
+
back.querySelector("span")!.textContent = task.title ?? stem(task.run ?? task.source);
|
|
370
|
+
back.title = `${task.title ?? stem(task.run ?? task.source)} · Return to task`;
|
|
371
|
+
back.addEventListener("click", () => {
|
|
372
|
+
taskFilter = "all";
|
|
373
|
+
switchTo("tasks", false);
|
|
374
|
+
renderTasks();
|
|
375
|
+
taskList.querySelector<HTMLElement>(`[data-task-id="${CSS.escape(task.id)}"]`)?.scrollIntoView({ block: "nearest" });
|
|
376
|
+
});
|
|
377
|
+
const clear = document.createElement("button");
|
|
378
|
+
clear.type = "button"; clear.className = "task-context-clear";
|
|
379
|
+
clear.innerHTML = icon("close"); clear.title = "Clear task filter"; clear.setAttribute("aria-label", clear.title);
|
|
380
|
+
clear.addEventListener("click", () => {
|
|
381
|
+
selectedTask = undefined; delete libraries.artifacts; renderTaskContext(); renderTasks();
|
|
382
|
+
if (active === "artifacts") { renderArtifacts(); void refresh("artifacts"); }
|
|
383
|
+
});
|
|
384
|
+
node.append(back, clear);
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
241
388
|
const renderTasks = (): void => {
|
|
389
|
+
for (const node of Array.from(taskList.querySelectorAll("[data-more-section]"))) morePages.unobserve(node);
|
|
390
|
+
const library = libraries.tasks;
|
|
242
391
|
const tasks = library?.tasks ?? [];
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
392
|
+
const filters = [["all", "All tasks", "component"], ["ongoing", "In progress", "when"], ["finished", "Finished", "check"]] as const;
|
|
393
|
+
const heading = taskPanel.toolbar.querySelector<HTMLElement>("[data-task-heading]")!;
|
|
394
|
+
heading.textContent = filters.find(([id]) => id === taskFilter)![1];
|
|
395
|
+
heading.title = library?.runtime === undefined ? "Saved project Results · no Runtime selected" : library.runtime;
|
|
396
|
+
taskPanel.navigation.replaceChildren(...filters.map(([id, label, mark]) => sidebarItem({
|
|
397
|
+
label, icon: mark, selected: id === taskFilter,
|
|
398
|
+
select: () => { taskFilter = id; taskList.scrollTop = 0; renderTasks(); },
|
|
399
|
+
})));
|
|
400
|
+
const tasksToShow = tasks.filter((task) => taskFilter === "all" || task.ongoing === (taskFilter === "ongoing"))
|
|
401
|
+
.sort((left, right) => Number(right.ongoing) - Number(left.ongoing) || right.createdAt - left.createdAt);
|
|
402
|
+
if (selectedTask !== undefined) selectedTask = tasks.find((task) => task.id === selectedTask!.id) ?? selectedTask;
|
|
403
|
+
const selectTask = (task: StudioTaskView): void => {
|
|
404
|
+
const changed = selectedTask?.id !== task.id;
|
|
405
|
+
selectedTask = task;
|
|
406
|
+
if (changed) delete libraries.artifacts;
|
|
407
|
+
renderTaskContext();
|
|
408
|
+
for (const card of Array.from(taskList.querySelectorAll<HTMLElement>("[data-task-id]"))) {
|
|
409
|
+
card.classList.toggle("selected", card.dataset.taskId === task.id);
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
const entries = tasksToShow.map((task) => taskCard(task, selectedTask?.id === task.id,
|
|
413
|
+
() => selectTask(task), () => { selectTask(task); switchTo("artifacts"); }));
|
|
414
|
+
renderTaskContext();
|
|
415
|
+
if (tasksToShow.length === 0) entries.push(emptyState("tasks",
|
|
416
|
+
library === undefined && request !== undefined ? "Loading tasks…" : taskFilter === "ongoing" ? "No tasks in progress" : "No tasks here",
|
|
417
|
+
taskFilter === "ongoing" && library?.runtime === undefined
|
|
418
|
+
? "No Runtime is selected. Saved tasks are available under Finished."
|
|
419
|
+
: "Refresh to read the latest task status."));
|
|
420
|
+
// Runtime activity is returned in full on the first page. Older pages contain Results only.
|
|
421
|
+
if (taskFilter !== "ongoing" && library?.next !== undefined) entries.push(continuation("tasks", library.next));
|
|
262
422
|
taskList.replaceChildren(...entries);
|
|
423
|
+
for (const node of Array.from(taskList.querySelectorAll("[data-more-section]"))) morePages.observe(node);
|
|
263
424
|
};
|
|
264
425
|
|
|
265
426
|
const renderArtifacts = (): void => {
|
|
427
|
+
videoPreviews.disconnect();
|
|
428
|
+
for (const node of Array.from(artifactGrid.querySelectorAll("[data-more-section]"))) morePages.unobserve(node);
|
|
429
|
+
const cards = new Map(Array.from(artifactGrid.querySelectorAll<HTMLElement>("[data-artifact-id]"), (card) => [card.dataset.artifactId!, card]));
|
|
430
|
+
const library = libraries.artifacts;
|
|
266
431
|
const artifacts = library?.artifacts ?? [];
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}));
|
|
282
|
-
const shown = artifactFilter === "all"
|
|
283
|
-
? artifacts
|
|
432
|
+
const filters = [["all", "All media", "component"], ["video", "Videos", "video"], ["image", "Images", "image"], ["audio", "Audio", "waveform"]] as const;
|
|
433
|
+
const heading = mediaPanel.toolbar.querySelector<HTMLElement>("[data-media-heading]")!;
|
|
434
|
+
heading.textContent = filters.find(([id]) => id === artifactFilter)?.[1] ?? "All media";
|
|
435
|
+
heading.title = "Media category";
|
|
436
|
+
renderTaskContext();
|
|
437
|
+
artifactFilters.replaceChildren(...filters.map(([id, label, mark]) => sidebarItem({
|
|
438
|
+
label, icon: mark, selected: id === artifactFilter,
|
|
439
|
+
select: () => {
|
|
440
|
+
artifactFilter = id;
|
|
441
|
+
delete libraries.artifacts;
|
|
442
|
+
artifactGrid.scrollTop = 0;
|
|
443
|
+
void refresh("artifacts");
|
|
444
|
+
renderArtifacts();
|
|
445
|
+
},
|
|
446
|
+
})));
|
|
447
|
+
const shown = artifactFilter === "all" ? artifacts
|
|
284
448
|
: artifacts.filter((artifact) => artifact.mediaType.startsWith(`${artifactFilter}/`));
|
|
285
|
-
const entries: HTMLElement[] = shown.map(
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
449
|
+
const entries: HTMLElement[] = shown.map((artifact) => {
|
|
450
|
+
const held = cards.get(artifact.id);
|
|
451
|
+
cards.delete(artifact.id);
|
|
452
|
+
if (held !== undefined && held.dataset.build === artifact.build && held.dataset.output === artifact.output
|
|
453
|
+
&& held.dataset.displayName === (artifact.displayName ?? "") && held.dataset.nameEditable === String(artifact.nameEditable === true)) return held;
|
|
454
|
+
const previousVideo = held?.querySelector("video");
|
|
455
|
+
if (previousVideo != null) { previousVideo.removeAttribute("src"); previousVideo.load(); }
|
|
456
|
+
return artifactCard(artifact, openArtifact, (updated) => {
|
|
457
|
+
const current = libraries.artifacts;
|
|
458
|
+
if (current !== undefined) libraries.artifacts = { ...current,
|
|
459
|
+
artifacts: current.artifacts.map((item) => item.id === updated.id ? { ...item, displayName: updated.displayName! } : item),
|
|
460
|
+
};
|
|
461
|
+
renamedArtifact(updated);
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
for (const card of cards.values()) {
|
|
465
|
+
const video = card.querySelector("video");
|
|
466
|
+
if (video !== null) { video.removeAttribute("src"); video.load(); }
|
|
290
467
|
}
|
|
291
|
-
if (
|
|
468
|
+
if (shown.length === 0) entries.push(emptyState("results",
|
|
469
|
+
library === undefined && request !== undefined ? "Loading media…" : artifactFilter === "all" ? "No media" : `No ${artifactFilter} files`,
|
|
470
|
+
"Images, videos and audio published as file Outputs appear here, including those from ongoing Builds."));
|
|
471
|
+
if (library?.next !== undefined) entries.push(continuation("artifacts", library.next));
|
|
292
472
|
artifactGrid.replaceChildren(...entries);
|
|
473
|
+
selectArtifact(selectedArtifact);
|
|
474
|
+
for (const preview of Array.from(artifactGrid.querySelectorAll("video:not([src]), [data-audio-source]:not(.is-ready)"))) videoPreviews.observe(preview);
|
|
475
|
+
for (const node of Array.from(artifactGrid.querySelectorAll("[data-more-section]"))) morePages.observe(node);
|
|
293
476
|
};
|
|
294
477
|
|
|
295
|
-
const switchTo = (next: LibrarySection): void => {
|
|
478
|
+
const switchTo = (next: LibrarySection, reload = true): void => {
|
|
296
479
|
active = next;
|
|
297
480
|
for (const tab of Array.from(element.querySelectorAll<HTMLButtonElement>("[data-library-tab]"))) {
|
|
298
481
|
const selected = tab.dataset.libraryTab === next;
|
|
@@ -302,77 +485,75 @@ export function createLibraryPane(code: CodePane): LibraryPane {
|
|
|
302
485
|
for (const view of Array.from(element.querySelectorAll<HTMLElement>("[data-library-view]"))) {
|
|
303
486
|
view.classList.toggle("active", view.dataset.libraryView === next);
|
|
304
487
|
}
|
|
305
|
-
if (next
|
|
488
|
+
if (next === "artifacts") renderArtifacts();
|
|
489
|
+
if (next === "tasks") renderTasks();
|
|
490
|
+
if (next !== "source" && reload) void refresh(next);
|
|
491
|
+
else request?.abort();
|
|
306
492
|
};
|
|
307
493
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
const tasks = new Map(library.tasks.map((task) => [task.id, task]));
|
|
318
|
-
const artifacts = new Map(library.artifacts.map((artifact) => [artifact.id, artifact]));
|
|
319
|
-
for (const task of value.tasks) tasks.set(task.id, task);
|
|
320
|
-
for (const artifact of value.artifacts) artifacts.set(artifact.id, artifact);
|
|
321
|
-
library = {
|
|
322
|
-
environment: value.environment,
|
|
323
|
-
...(value.runtime === undefined ? {} : { runtime: value.runtime }),
|
|
324
|
-
...((append || !expanded ? value.next : library.next) === undefined
|
|
325
|
-
? {}
|
|
326
|
-
: { next: append || !expanded ? value.next : library.next }),
|
|
327
|
-
tasks: [...tasks.values()].sort((left, right) => right.createdAt - left.createdAt || left.id.localeCompare(right.id)),
|
|
328
|
-
artifacts: [...artifacts.values()].sort((left, right) =>
|
|
329
|
-
Number(right.highlighted) - Number(left.highlighted)
|
|
330
|
-
|| right.createdAt - left.createdAt
|
|
331
|
-
|| left.output.localeCompare(right.output)
|
|
332
|
-
|| left.valuePath.localeCompare(right.valuePath)),
|
|
333
|
-
};
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
const refresh = async (before?: string): Promise<void> => {
|
|
337
|
-
if (refreshing) return;
|
|
338
|
-
refreshing = true;
|
|
494
|
+
const refresh = async (section: "tasks" | "artifacts", before?: string): Promise<void> => {
|
|
495
|
+
if (before !== undefined && request !== undefined) return;
|
|
496
|
+
request?.abort();
|
|
497
|
+
const controller = new AbortController();
|
|
498
|
+
request = controller;
|
|
499
|
+
const status = element.querySelector<HTMLElement>(`[data-library-status="${section}"]`)!;
|
|
500
|
+
status.textContent = "Refreshing…";
|
|
501
|
+
status.hidden = true;
|
|
339
502
|
element.classList.add("is-refreshing");
|
|
340
503
|
try {
|
|
341
|
-
const query =
|
|
342
|
-
|
|
504
|
+
const query = new URLSearchParams({ section });
|
|
505
|
+
if (before !== undefined) query.set("before", before);
|
|
506
|
+
if (section === "artifacts" && artifactFilter !== "all") query.set("media", artifactFilter);
|
|
507
|
+
if (section === "artifacts" && selectedTask !== undefined) query.set("build", selectedTask.id);
|
|
508
|
+
const response = await fetch(`/__studio/library?${query.toString()}`, { signal: controller.signal });
|
|
343
509
|
const value = await response.json() as StudioLibraryView | { readonly error: string };
|
|
344
510
|
if (!response.ok || "error" in value) throw new Error("error" in value ? value.error : "Library unavailable");
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
511
|
+
if (controller.signal.aborted) return;
|
|
512
|
+
const previous = libraries[section];
|
|
513
|
+
if (before === undefined || previous === undefined) libraries[section] = value;
|
|
514
|
+
else {
|
|
515
|
+
const tasks = new Map(previous.tasks.map((task) => [task.id, task]));
|
|
516
|
+
for (const task of value.tasks) tasks.set(task.id, task);
|
|
517
|
+
libraries[section] = {
|
|
518
|
+
...value,
|
|
519
|
+
tasks: [...tasks.values()].sort((left, right) => right.createdAt - left.createdAt || left.id.localeCompare(right.id)),
|
|
520
|
+
artifacts: mergeStudioArtifacts([...previous.artifacts, ...value.artifacts]),
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
if (section === "tasks") renderTasks();
|
|
524
|
+
else renderArtifacts();
|
|
525
|
+
status.textContent = `Updated ${new Date().toLocaleTimeString()}`;
|
|
349
526
|
} catch (error) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
527
|
+
if (!controller.signal.aborted) {
|
|
528
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
529
|
+
status.hidden = false;
|
|
530
|
+
status.textContent = `Refresh failed: ${detail}${libraries[section] === undefined ? "" : " · Previous view retained"}`;
|
|
531
|
+
}
|
|
353
532
|
} finally {
|
|
354
|
-
|
|
355
|
-
|
|
533
|
+
if (request === controller) {
|
|
534
|
+
request = undefined;
|
|
535
|
+
element.classList.remove("is-refreshing");
|
|
536
|
+
if (controller.signal.aborted) status.textContent = "Refresh interrupted";
|
|
537
|
+
}
|
|
356
538
|
}
|
|
357
539
|
};
|
|
358
540
|
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
541
|
+
for (const tab of Array.from(element.querySelectorAll<HTMLButtonElement>("[data-library-tab]"))) {
|
|
542
|
+
tab.addEventListener("click", () => {
|
|
543
|
+
switchTo(tab.dataset.libraryTab as LibrarySection, tab.dataset.libraryTab !== "tasks" || libraries.tasks === undefined);
|
|
544
|
+
});
|
|
545
|
+
}
|
|
363
546
|
for (const button of Array.from(element.querySelectorAll<HTMLButtonElement>("[data-library-refresh]"))) {
|
|
364
|
-
button.addEventListener("click", () => void refresh());
|
|
547
|
+
button.addEventListener("click", () => { if (active !== "source") void refresh(active); });
|
|
365
548
|
}
|
|
366
|
-
window.setInterval(() => {
|
|
367
|
-
if (document.visibilityState === "visible" && active !== "source") void refresh();
|
|
368
|
-
}, 3_000);
|
|
369
549
|
|
|
370
550
|
return {
|
|
371
551
|
element,
|
|
552
|
+
selectArtifact,
|
|
372
553
|
show(value) {
|
|
373
554
|
snapshot = value;
|
|
374
555
|
renderSources();
|
|
375
556
|
},
|
|
376
|
-
refresh,
|
|
557
|
+
refresh: async () => { if (active !== "source") await refresh(active); },
|
|
377
558
|
};
|
|
378
559
|
}
|