openclacky 1.3.11 → 1.4.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +59 -0
- data/lib/clacky/agent/session_serializer.rb +11 -4
- data/lib/clacky/agent/skill_manager.rb +1 -0
- data/lib/clacky/agent.rb +4 -5
- data/lib/clacky/brand_config.rb +53 -3
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +202 -3
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +610 -60
- data/lib/clacky/default_skills/media-gen/SKILL.md +182 -7
- data/lib/clacky/default_skills/media-gen/scripts/video_seq.sh +5 -3
- data/lib/clacky/extension/packager.rb +21 -1
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/media/generator.rb +41 -0
- data/lib/clacky/media/volcengine.rb +394 -0
- data/lib/clacky/platform_http_client.rb +9 -7
- data/lib/clacky/providers.rb +4 -4
- data/lib/clacky/server/http_server.rb +67 -22
- data/lib/clacky/tools/grep.rb +6 -1
- data/lib/clacky/tools/terminal.rb +24 -8
- data/lib/clacky/ui2/layout_manager.rb +4 -0
- data/lib/clacky/ui2/screen_buffer.rb +31 -3
- data/lib/clacky/utils/environment_detector.rb +16 -9
- data/lib/clacky/utils/file_processor.rb +30 -28
- data/lib/clacky/utils/model_pricing.rb +49 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +229 -31
- data/lib/clacky/web/components/onboard.js +14 -4
- data/lib/clacky/web/features/backup/view.js +4 -3
- data/lib/clacky/web/features/extensions/store.js +2 -2
- data/lib/clacky/web/features/extensions/view.js +37 -7
- data/lib/clacky/web/features/mcp/view.js +5 -2
- data/lib/clacky/web/features/skills/view.js +61 -1
- data/lib/clacky/web/features/workspace/store.js +11 -0
- data/lib/clacky/web/features/workspace/view.js +10 -4
- data/lib/clacky/web/i18n.js +14 -2
- data/lib/clacky/web/index.html +29 -13
- data/lib/clacky/web/sessions.js +45 -8
- data/scripts/uninstall.sh +1 -1
- metadata +2 -1
|
@@ -49,6 +49,13 @@
|
|
|
49
49
|
"published.confirm": "Unpublish {{id}} from the marketplace?",
|
|
50
50
|
"err.generic": "Something went wrong: {{msg}}",
|
|
51
51
|
"err.version_conflict": "Version {{ver}} has already been published. Please enter a higher version number above and try again.",
|
|
52
|
+
"err.invalid_version": "Invalid version. Use format like 1.0.0.",
|
|
53
|
+
"err.name_taken": "The name \"{{id}}\" is already taken by another user.",
|
|
54
|
+
"rename.label": "Choose a new ID for your extension:",
|
|
55
|
+
"rename.placeholder": "e.g. my-ext-yourname",
|
|
56
|
+
"rename.hint": "Only lowercase letters, digits and hyphens. This will rename the local folder and update ext.yml.",
|
|
57
|
+
"rename.btn": "Rename & Publish",
|
|
58
|
+
"rename.renaming": "Renaming…",
|
|
52
59
|
"pub.version.label": "Version",
|
|
53
60
|
|
|
54
61
|
"publish.confirm.title": "Publish to the marketplace?",
|
|
@@ -88,6 +95,27 @@
|
|
|
88
95
|
"pub.success": "Published successfully",
|
|
89
96
|
"pub.done.close": "Done",
|
|
90
97
|
"pub.retry": "Try again",
|
|
98
|
+
"pub.readme.placeholder": "## Overview\n\nDescribe what your extension does...",
|
|
99
|
+
"pub.screenshot.upload": "Upload screenshot",
|
|
100
|
+
"pub.screenshot.uploading": "Uploading…",
|
|
101
|
+
"pub.screenshot.section": "Screenshots",
|
|
102
|
+
"pub.screenshot.hint": "PNG / JPG / GIF, up to 5 MB each",
|
|
103
|
+
"pub.screenshot.err": "Upload failed: {{msg}}",
|
|
104
|
+
"pub.screenshot.err.type": "Only PNG / JPG / GIF are allowed",
|
|
105
|
+
"pub.screenshot.err.size": "File size must be under 5 MB",
|
|
106
|
+
"pub.readme.edit.title": "Edit README",
|
|
107
|
+
"readme.pane.editor": "Markdown editor",
|
|
108
|
+
"readme.pane.preview": "Preview",
|
|
109
|
+
"pub.readme.edit": "Edit",
|
|
110
|
+
"pub.readme.section": "Description",
|
|
111
|
+
"pub.readme.empty": "No description yet — click Edit to add one.",
|
|
112
|
+
"pub.readme.save.empty": "Description cannot be empty.",
|
|
113
|
+
"pub.readme.required": "Please add a description (README) before publishing.",
|
|
114
|
+
"pub.readme.required.skip": "Skip, publish anyway",
|
|
115
|
+
"pub.readme.required.edit": "Add description",
|
|
116
|
+
"pub.readme.save": "Save",
|
|
117
|
+
"pub.readme.saving": "Saving…",
|
|
118
|
+
"pub.readme.skip": "Skip",
|
|
91
119
|
"extlist.section.cloud": "Published Extensions",
|
|
92
120
|
"extlist.section.cloudHint": "Live on the marketplace",
|
|
93
121
|
"extlist.section.local": "Local Extensions",
|
|
@@ -118,6 +146,25 @@
|
|
|
118
146
|
"extlist.newExt.hint": "Opens an AI session that scaffolds and builds it for you.",
|
|
119
147
|
"extlist.newExt.btn": "Create New Extension",
|
|
120
148
|
"extlist.newExt.seed": "I want to build a new OpenClacky extension. Help me get started: ask me what the extension should do, then scaffold it and build it out step by step.",
|
|
149
|
+
"unit.panel": "panel",
|
|
150
|
+
"unit.panels": "panels",
|
|
151
|
+
"unit.agent": "agent",
|
|
152
|
+
"unit.agents": "agents",
|
|
153
|
+
"unit.api": "API",
|
|
154
|
+
"unit.apis": "APIs",
|
|
155
|
+
"unit.skill": "skill",
|
|
156
|
+
"unit.skills": "skills",
|
|
157
|
+
|
|
158
|
+
"meta.section": "Extension Info",
|
|
159
|
+
"meta.edit.title": "Edit Extension Info",
|
|
160
|
+
"meta.edit": "Edit",
|
|
161
|
+
"meta.name.label": "Display name",
|
|
162
|
+
"meta.desc.label": "Description",
|
|
163
|
+
"meta.save": "Save",
|
|
164
|
+
"meta.saving": "Saving…",
|
|
165
|
+
"meta.saved": "Saved",
|
|
166
|
+
"meta.save.err": "Save failed: {{msg}}",
|
|
167
|
+
|
|
121
168
|
"skills.section.cloud": "Cloud Skills",
|
|
122
169
|
"skills.section.cloudHint": "Published to the platform",
|
|
123
170
|
"skills.section.local": "Local Skills",
|
|
@@ -186,6 +233,13 @@
|
|
|
186
233
|
"published.confirm": "确定要从市场下架 {{id}} 吗?",
|
|
187
234
|
"err.generic": "出错了:{{msg}}",
|
|
188
235
|
"err.version_conflict": "版本 {{ver}} 已发布过,请在上方输入更高的版本号后重试。",
|
|
236
|
+
"err.invalid_version": "版本号格式不对,请使用如 1.0.0 的格式。",
|
|
237
|
+
"err.name_taken": "扩展名「{{id}}」已被其他用户占用。",
|
|
238
|
+
"rename.label": "请为你的扩展选择一个新 ID:",
|
|
239
|
+
"rename.placeholder": "例如 my-ext-yourname",
|
|
240
|
+
"rename.hint": "只能使用小写字母、数字和连字符。",
|
|
241
|
+
"rename.btn": "重命名并发布",
|
|
242
|
+
"rename.renaming": "重命名中…",
|
|
189
243
|
"pub.version.label": "版本号",
|
|
190
244
|
|
|
191
245
|
"publish.confirm.title": "确定发布到市场?",
|
|
@@ -225,6 +279,27 @@
|
|
|
225
279
|
"pub.success": "已发布成功",
|
|
226
280
|
"pub.done.close": "完成",
|
|
227
281
|
"pub.retry": "重试",
|
|
282
|
+
"pub.readme.placeholder": "## 简介\n\n描述你的扩展的功能…",
|
|
283
|
+
"pub.screenshot.upload": "上传截图",
|
|
284
|
+
"pub.screenshot.uploading": "上传中…",
|
|
285
|
+
"pub.screenshot.section": "截图",
|
|
286
|
+
"pub.screenshot.hint": "PNG / JPG / GIF,每张最大 5 MB",
|
|
287
|
+
"pub.screenshot.err": "上传失败:{{msg}}",
|
|
288
|
+
"pub.screenshot.err.type": "仅支持 PNG / JPG / GIF 格式",
|
|
289
|
+
"pub.screenshot.err.size": "文件大小不能超过 5 MB",
|
|
290
|
+
"pub.readme.edit.title": "编辑使用说明",
|
|
291
|
+
"readme.pane.editor": "Markdown 编写",
|
|
292
|
+
"readme.pane.preview": "预览",
|
|
293
|
+
"pub.readme.edit": "编辑",
|
|
294
|
+
"pub.readme.section": "使用说明",
|
|
295
|
+
"pub.readme.empty": "暂无说明 — 点击编辑添加。",
|
|
296
|
+
"pub.readme.save.empty": "使用说明不能为空。",
|
|
297
|
+
"pub.readme.required": "发布前请先填写使用说明。",
|
|
298
|
+
"pub.readme.required.skip": "忽略,继续发布",
|
|
299
|
+
"pub.readme.required.edit": "去填写",
|
|
300
|
+
"pub.readme.save": "保存",
|
|
301
|
+
"pub.readme.saving": "保存中…",
|
|
302
|
+
"pub.readme.skip": "跳过",
|
|
228
303
|
"extlist.section.cloud": "已发布扩展",
|
|
229
304
|
"extlist.section.cloudHint": "已上架到市场",
|
|
230
305
|
"extlist.section.local": "本地扩展",
|
|
@@ -255,6 +330,25 @@
|
|
|
255
330
|
"extlist.newExt.hint": "打开一个 AI 会话,帮你生成并开发它。",
|
|
256
331
|
"extlist.newExt.btn": "新建扩展",
|
|
257
332
|
"extlist.newExt.seed": "我想开发一个新的 OpenClacky 扩展。请引导我开始:先问清楚这个扩展要做什么,然后帮我搭好骨架并一步步开发出来。",
|
|
333
|
+
"unit.panel": "个面板",
|
|
334
|
+
"unit.panels": "个面板",
|
|
335
|
+
"unit.agent": "个 Agent",
|
|
336
|
+
"unit.agents": "个 Agent",
|
|
337
|
+
"unit.api": "个 API",
|
|
338
|
+
"unit.apis": "个 API",
|
|
339
|
+
"unit.skill": "个技能",
|
|
340
|
+
"unit.skills": "个技能",
|
|
341
|
+
|
|
342
|
+
"meta.section": "扩展信息",
|
|
343
|
+
"meta.edit.title": "编辑扩展信息",
|
|
344
|
+
"meta.edit": "编辑",
|
|
345
|
+
"meta.name.label": "显示名称",
|
|
346
|
+
"meta.desc.label": "描述",
|
|
347
|
+
"meta.save": "保存",
|
|
348
|
+
"meta.saving": "保存中…",
|
|
349
|
+
"meta.saved": "已保存",
|
|
350
|
+
"meta.save.err": "保存失败:{{msg}}",
|
|
351
|
+
|
|
258
352
|
"skills.section.cloud": "云端 Skills",
|
|
259
353
|
"skills.section.cloudHint": "已发布到平台",
|
|
260
354
|
"skills.section.local": "本地 Skills",
|
|
@@ -316,6 +410,22 @@
|
|
|
316
410
|
return data;
|
|
317
411
|
}
|
|
318
412
|
|
|
413
|
+
async function patchJson(path, body) {
|
|
414
|
+
const res = await fetch(api(path), {
|
|
415
|
+
method: "PATCH",
|
|
416
|
+
headers: { "Content-Type": "application/json" },
|
|
417
|
+
body: JSON.stringify(body || {}),
|
|
418
|
+
});
|
|
419
|
+
const data = await res.json();
|
|
420
|
+
if (!res.ok) {
|
|
421
|
+
const err = new Error(data.error || `Request failed (${res.status})`);
|
|
422
|
+
err.status = res.status;
|
|
423
|
+
err.data = data;
|
|
424
|
+
throw err;
|
|
425
|
+
}
|
|
426
|
+
return data;
|
|
427
|
+
}
|
|
428
|
+
|
|
319
429
|
async function downloadPack(ext_id) {
|
|
320
430
|
const res = await fetch(api("/pack"), {
|
|
321
431
|
method: "POST",
|
|
@@ -383,7 +493,6 @@
|
|
|
383
493
|
box.appendChild(bodyEl);
|
|
384
494
|
box.appendChild(footer);
|
|
385
495
|
overlay.appendChild(box);
|
|
386
|
-
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
|
|
387
496
|
document.body.appendChild(overlay);
|
|
388
497
|
return { overlay, bodyEl, footer, close };
|
|
389
498
|
}
|
|
@@ -477,13 +586,16 @@
|
|
|
477
586
|
}
|
|
478
587
|
}
|
|
479
588
|
|
|
589
|
+
function isSemver(v) {
|
|
590
|
+
return /^\d+\.\d+\.\d+$/.test(v);
|
|
591
|
+
}
|
|
592
|
+
|
|
480
593
|
// Unified publish flow: one modal that walks the creator from a release form
|
|
481
|
-
// (version + notes) through progress →
|
|
482
|
-
//
|
|
483
|
-
// version they can bump it inline — the modal writes it back to ext.yml
|
|
484
|
-
// before uploading. `prevVersionOrPromise` may be a value or a Promise.
|
|
594
|
+
// (version + notes) through progress → done.
|
|
595
|
+
// `prevVersionOrPromise` may be a value or a Promise.
|
|
485
596
|
// Resolves true when a publish succeeded, false otherwise.
|
|
486
|
-
|
|
597
|
+
|
|
598
|
+
function runPublishFlow(ext, prevVersionOrPromise, { onPublishDone } = {}) {
|
|
487
599
|
return new Promise((resolve) => {
|
|
488
600
|
let currentVersion = ext.version || "";
|
|
489
601
|
let isUpdate = false;
|
|
@@ -493,8 +605,15 @@
|
|
|
493
605
|
const verInput = el("input", { class: "studio-input", type: "text", value: currentVersion, placeholder: "1.0.0" });
|
|
494
606
|
verInput.addEventListener("input", () => {
|
|
495
607
|
currentVersion = verInput.value.trim();
|
|
496
|
-
|
|
497
|
-
|
|
608
|
+
const valid = isSemver(currentVersion);
|
|
609
|
+
verInput.classList.toggle("studio-input-error", !!currentVersion && !valid);
|
|
610
|
+
publishBtn.disabled = !valid;
|
|
611
|
+
publishBtn.textContent = valid ? t("pub.btn.publish", { ver: currentVersion }) : t("pub.btn.publish", { ver: currentVersion || "?" });
|
|
612
|
+
if (currentVersion && !valid) {
|
|
613
|
+
setProgress(t("err.invalid_version"), true);
|
|
614
|
+
} else {
|
|
615
|
+
status.style.display = "none";
|
|
616
|
+
}
|
|
498
617
|
});
|
|
499
618
|
verField.appendChild(verInput);
|
|
500
619
|
|
|
@@ -563,6 +682,13 @@
|
|
|
563
682
|
if (done || publishBtn.disabled) return;
|
|
564
683
|
const ver = currentVersion;
|
|
565
684
|
if (!ver) return;
|
|
685
|
+
if (!isSemver(ver)) {
|
|
686
|
+
setProgress(t("err.invalid_version", { ver }), true);
|
|
687
|
+
verInput.classList.add("studio-input-error");
|
|
688
|
+
verInput.focus();
|
|
689
|
+
verInput.select();
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
566
692
|
|
|
567
693
|
publishBtn.disabled = true;
|
|
568
694
|
publishBtn.textContent = t("pub.btn.publishing");
|
|
@@ -583,15 +709,20 @@
|
|
|
583
709
|
});
|
|
584
710
|
|
|
585
711
|
if (data === null) { resetButtons(); return; }
|
|
712
|
+
|
|
713
|
+
// Name taken by another user — show inline rename form.
|
|
714
|
+
if (!data.ok && data.name_taken) {
|
|
715
|
+
showRenameForm(ext, data.error);
|
|
716
|
+
modal.close();
|
|
717
|
+
resolve(false);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
|
|
586
721
|
if (!data.ok) throw new Error(data.error || "Publish failed");
|
|
587
722
|
|
|
588
723
|
done = true;
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
cancelBtn.style.display = "none";
|
|
592
|
-
publishBtn.disabled = false;
|
|
593
|
-
publishBtn.textContent = t("pub.btn.done");
|
|
594
|
-
publishBtn.onclick = () => { modal.close(); resolve(true); };
|
|
724
|
+
modal.close();
|
|
725
|
+
resolve(true);
|
|
595
726
|
} catch (e) {
|
|
596
727
|
const msg = e.message || "";
|
|
597
728
|
const isConflict = /must be greater than/i.test(msg);
|
|
@@ -611,7 +742,83 @@
|
|
|
611
742
|
});
|
|
612
743
|
}
|
|
613
744
|
|
|
614
|
-
//
|
|
745
|
+
// Show an inline modal that lets the creator pick a new extension ID when
|
|
746
|
+
// the current name is already taken by another user on the marketplace.
|
|
747
|
+
// On confirm: calls set_id to rename the local folder + ext.yml, then
|
|
748
|
+
// immediately re-triggers the full publish flow with the new id.
|
|
749
|
+
function showRenameForm(ext, serverError) {
|
|
750
|
+
const slugFormat = /^[a-z0-9][a-z0-9\-]*[a-z0-9]$/;
|
|
751
|
+
|
|
752
|
+
const errorMsg = el("p", { class: "studio-modal-status studio-modal-status-error",
|
|
753
|
+
text: t("err.name_taken", { id: ext.id }) });
|
|
754
|
+
|
|
755
|
+
const field = el("div", { class: "studio-field" });
|
|
756
|
+
field.appendChild(el("label", { class: "studio-label", text: t("rename.label") }));
|
|
757
|
+
const input = el("input", { class: "studio-input", type: "text",
|
|
758
|
+
value: ext.id, placeholder: t("rename.placeholder") });
|
|
759
|
+
const hint = el("p", { class: "studio-field-hint", text: t("rename.hint") });
|
|
760
|
+
field.appendChild(input);
|
|
761
|
+
field.appendChild(hint);
|
|
762
|
+
|
|
763
|
+
const status = el("p", { class: "studio-modal-status" });
|
|
764
|
+
status.style.display = "none";
|
|
765
|
+
|
|
766
|
+
let done = false;
|
|
767
|
+
const modal = openModal({
|
|
768
|
+
title: t("rename.btn"),
|
|
769
|
+
body: el("div", null, [field, errorMsg, status]),
|
|
770
|
+
buttons: [
|
|
771
|
+
{ label: t("pub.btn.cancel"), keepOpen: false, onClick: () => {} },
|
|
772
|
+
{ label: t("rename.btn"), primary: true, keepOpen: true, onClick: () => submit() },
|
|
773
|
+
],
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
const confirmBtn = modal.footer.querySelector(".studio-btn-primary");
|
|
777
|
+
const cancelBtn = modal.footer.querySelector(".studio-btn:not(.studio-btn-primary)");
|
|
778
|
+
|
|
779
|
+
input.addEventListener("input", () => {
|
|
780
|
+
const val = input.value.trim();
|
|
781
|
+
const valid = val.length >= 2 && val.length <= 64 && slugFormat.test(val) && val !== ext.id;
|
|
782
|
+
confirmBtn.disabled = !valid;
|
|
783
|
+
input.classList.toggle("studio-input-error", !!val && !slugFormat.test(val));
|
|
784
|
+
status.style.display = "none";
|
|
785
|
+
});
|
|
786
|
+
// Disable confirm until user types a valid different id.
|
|
787
|
+
confirmBtn.disabled = true;
|
|
788
|
+
|
|
789
|
+
async function submit() {
|
|
790
|
+
if (done || confirmBtn.disabled) return;
|
|
791
|
+
const newId = input.value.trim();
|
|
792
|
+
confirmBtn.disabled = true;
|
|
793
|
+
confirmBtn.textContent = t("rename.renaming");
|
|
794
|
+
cancelBtn.disabled = true;
|
|
795
|
+
input.disabled = true;
|
|
796
|
+
status.style.display = "none";
|
|
797
|
+
|
|
798
|
+
try {
|
|
799
|
+
await postJson("/set_id", { ext_id: ext.id, new_id: newId });
|
|
800
|
+
done = true;
|
|
801
|
+
modal.close();
|
|
802
|
+
// Re-trigger the full publish flow with the renamed extension.
|
|
803
|
+
// Reload store first so the new id is visible.
|
|
804
|
+
await store.reload();
|
|
805
|
+
const renamed = store.extensions.find((e) => e.id === newId);
|
|
806
|
+
if (renamed) {
|
|
807
|
+
const ok = await runPublishFlow(renamed, null, { skipEntryCheck: true });
|
|
808
|
+
if (ok) store.reload();
|
|
809
|
+
}
|
|
810
|
+
} catch (e) {
|
|
811
|
+
status.textContent = t("err.generic", { msg: e.message });
|
|
812
|
+
status.style.display = "";
|
|
813
|
+
status.className = "studio-modal-status studio-modal-status-error";
|
|
814
|
+
confirmBtn.disabled = false;
|
|
815
|
+
confirmBtn.textContent = t("rename.btn");
|
|
816
|
+
cancelBtn.disabled = false;
|
|
817
|
+
input.disabled = false;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
615
822
|
// Both tabs care about "which local extension am I working on", so we keep a
|
|
616
823
|
// tiny page-level store and let each tab subscribe to changes.
|
|
617
824
|
const store = {
|
|
@@ -642,7 +849,7 @@
|
|
|
642
849
|
}
|
|
643
850
|
const select = el("select", { class: "studio-select" });
|
|
644
851
|
store.extensions.forEach((e) => {
|
|
645
|
-
const opt = el("option", { value: e.id, text: `${e.name} (${e.id})` });
|
|
852
|
+
const opt = el("option", { value: e.id, text: e.name ? `${e.name} (${e.id})` : e.id });
|
|
646
853
|
if (e.id === store.selectedId) opt.selected = true;
|
|
647
854
|
select.appendChild(opt);
|
|
648
855
|
});
|
|
@@ -713,32 +920,6 @@
|
|
|
713
920
|
if (box) { box.innerHTML = ""; }
|
|
714
921
|
}
|
|
715
922
|
|
|
716
|
-
function renderDetail() {
|
|
717
|
-
const detail = container.querySelector(".studio-detail");
|
|
718
|
-
if (!detail) return;
|
|
719
|
-
detail.innerHTML = "";
|
|
720
|
-
const ext = store.selected();
|
|
721
|
-
if (!ext) return;
|
|
722
|
-
|
|
723
|
-
detail.appendChild(el("h4", { class: "studio-detail-name", text: ext.name }));
|
|
724
|
-
if (ext.description) detail.appendChild(el("p", { class: "studio-detail-desc", text: ext.description }));
|
|
725
|
-
|
|
726
|
-
const meta = el("dl", { class: "studio-meta" });
|
|
727
|
-
const row = (label, value) => { meta.appendChild(el("dt", { text: label })); meta.appendChild(el("dd", { text: value || "—" })); };
|
|
728
|
-
row(t("detail.version"), ext.version);
|
|
729
|
-
row(t("detail.layer"), ext.layer);
|
|
730
|
-
detail.appendChild(meta);
|
|
731
|
-
|
|
732
|
-
detail.appendChild(el("div", { class: "studio-label", text: t("detail.units") }));
|
|
733
|
-
if (!ext.units.length) {
|
|
734
|
-
detail.appendChild(el("p", { class: "studio-empty", text: t("detail.noUnits") }));
|
|
735
|
-
} else {
|
|
736
|
-
const list = el("div", { class: "studio-units" });
|
|
737
|
-
ext.units.forEach((u) => list.appendChild(el("span", { class: "studio-unit-chip", text: `${u.kind} · ${u.id}` })));
|
|
738
|
-
detail.appendChild(list);
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
|
|
742
923
|
function rebuild() {
|
|
743
924
|
if (!container) return;
|
|
744
925
|
container.innerHTML = "";
|
|
@@ -749,8 +930,6 @@
|
|
|
749
930
|
return;
|
|
750
931
|
}
|
|
751
932
|
|
|
752
|
-
container.appendChild(el("div", { class: "studio-detail" }));
|
|
753
|
-
|
|
754
933
|
const bar = el("div", { class: "studio-actions" });
|
|
755
934
|
bar.appendChild(el("button", { class: "studio-btn studio-btn-primary studio-verify-status", text: t("btn.recheck"), onclick: runVerify }));
|
|
756
935
|
bar.appendChild(el("span", { class: "studio-verify-summary" }));
|
|
@@ -759,7 +938,6 @@
|
|
|
759
938
|
container.appendChild(el("div", { class: "studio-verify" }));
|
|
760
939
|
container.appendChild(el("p", { class: "studio-hint", text: t("hint.reload") }));
|
|
761
940
|
|
|
762
|
-
renderDetail();
|
|
763
941
|
runVerify();
|
|
764
942
|
}
|
|
765
943
|
|
|
@@ -777,6 +955,306 @@
|
|
|
777
955
|
}
|
|
778
956
|
|
|
779
957
|
// ── Publish tab ──────────────────────────────────────────────────────────
|
|
958
|
+
|
|
959
|
+
function buildExtInfoCard(ext, onEdit) {
|
|
960
|
+
const card = el("div", { class: "studio-detail" });
|
|
961
|
+
|
|
962
|
+
const head = el("div", { class: "studio-detail-head" });
|
|
963
|
+
head.appendChild(el("h4", { class: "studio-detail-name", text: ext.name || ext.id }));
|
|
964
|
+
const editBtn = el("button", { class: "studio-btn studio-btn-ghost studio-btn-sm", text: t("meta.edit") });
|
|
965
|
+
editBtn.addEventListener("click", onEdit);
|
|
966
|
+
head.appendChild(editBtn);
|
|
967
|
+
card.appendChild(head);
|
|
968
|
+
|
|
969
|
+
if (ext.description) card.appendChild(el("p", { class: "studio-detail-desc", text: ext.description }));
|
|
970
|
+
|
|
971
|
+
const infoRow = el("div", { class: "studio-info-row" });
|
|
972
|
+
infoRow.appendChild(el("span", { class: "studio-info-label", text: t("detail.version") }));
|
|
973
|
+
infoRow.appendChild(el("span", { class: "studio-info-value", text: ext.version || "—" }));
|
|
974
|
+
card.appendChild(infoRow);
|
|
975
|
+
|
|
976
|
+
card.appendChild(el("div", { class: "studio-label studio-section-label", text: t("detail.units") }));
|
|
977
|
+
if (!(ext.units || []).length) {
|
|
978
|
+
card.appendChild(el("p", { class: "studio-empty", text: t("detail.noUnits") }));
|
|
979
|
+
} else {
|
|
980
|
+
const list = el("div", { class: "studio-units" });
|
|
981
|
+
ext.units.forEach((u) => list.appendChild(el("span", { class: "studio-unit-chip", text: u.kind })));
|
|
982
|
+
card.appendChild(list);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
return card;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function buildReadmeCard(extId, currentReadme, onEdit) {
|
|
989
|
+
const card = el("div", { class: "studio-detail studio-readme-card" });
|
|
990
|
+
|
|
991
|
+
const head = el("div", { class: "studio-detail-head" });
|
|
992
|
+
head.appendChild(el("h4", { class: "studio-detail-name", text: t("pub.readme.section") }));
|
|
993
|
+
const editBtn = el("button", { class: "studio-btn studio-btn-ghost studio-btn-sm", text: t("pub.readme.edit") });
|
|
994
|
+
editBtn.addEventListener("click", onEdit);
|
|
995
|
+
head.appendChild(editBtn);
|
|
996
|
+
card.appendChild(head);
|
|
997
|
+
|
|
998
|
+
if (currentReadme && currentReadme.trim()) {
|
|
999
|
+
const preview = el("div", { class: "studio-readme-preview extension-readme-body studio-readme-card-preview" });
|
|
1000
|
+
preview.innerHTML = typeof marked !== "undefined"
|
|
1001
|
+
? marked.parse(currentReadme, { breaks: true, gfm: true })
|
|
1002
|
+
: `<pre>${currentReadme}</pre>`;
|
|
1003
|
+
card.appendChild(preview);
|
|
1004
|
+
} else {
|
|
1005
|
+
card.appendChild(el("p", { class: "studio-empty", text: t("pub.readme.empty") }));
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
return card;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function openMetaEditModal(ext, onSaved) {
|
|
1012
|
+
const overlay = el("div", { class: "studio-modal-overlay" });
|
|
1013
|
+
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
|
|
1014
|
+
|
|
1015
|
+
const modal = el("div", { class: "studio-modal" });
|
|
1016
|
+
overlay.appendChild(modal);
|
|
1017
|
+
document.body.appendChild(overlay);
|
|
1018
|
+
|
|
1019
|
+
function close() { document.body.removeChild(overlay); }
|
|
1020
|
+
|
|
1021
|
+
modal.appendChild(el("h3", { class: "studio-modal-title", text: t("meta.edit.title") }));
|
|
1022
|
+
|
|
1023
|
+
const nameField = el("div", { class: "studio-field" });
|
|
1024
|
+
nameField.appendChild(el("label", { class: "studio-label", text: t("meta.name.label") }));
|
|
1025
|
+
const nameInput = el("input", { class: "studio-input", value: ext.name || "" });
|
|
1026
|
+
nameField.appendChild(nameInput);
|
|
1027
|
+
modal.appendChild(nameField);
|
|
1028
|
+
|
|
1029
|
+
const descField = el("div", { class: "studio-field" });
|
|
1030
|
+
descField.appendChild(el("label", { class: "studio-label", text: t("meta.desc.label") }));
|
|
1031
|
+
const descInput = el("textarea", { class: "studio-textarea", rows: "5", text: ext.description || "" });
|
|
1032
|
+
descField.appendChild(descInput);
|
|
1033
|
+
modal.appendChild(descField);
|
|
1034
|
+
|
|
1035
|
+
const statusEl = el("span", { class: "studio-form-status" });
|
|
1036
|
+
const footer = el("div", { class: "studio-modal-footer" });
|
|
1037
|
+
const cancelBtn = el("button", { class: "studio-btn studio-btn-ghost", text: t("pub.btn.cancel"), onclick: close });
|
|
1038
|
+
const saveBtn = el("button", { class: "studio-btn studio-btn-primary", text: t("meta.save") });
|
|
1039
|
+
|
|
1040
|
+
saveBtn.addEventListener("click", async () => {
|
|
1041
|
+
saveBtn.disabled = true;
|
|
1042
|
+
saveBtn.textContent = t("meta.saving");
|
|
1043
|
+
statusEl.textContent = "";
|
|
1044
|
+
try {
|
|
1045
|
+
await postJson("/set_meta", {
|
|
1046
|
+
ext_id: ext.id,
|
|
1047
|
+
name: nameInput.value.trim(),
|
|
1048
|
+
description: descInput.value.trim(),
|
|
1049
|
+
});
|
|
1050
|
+
close();
|
|
1051
|
+
if (onSaved) onSaved();
|
|
1052
|
+
} catch (e) {
|
|
1053
|
+
saveBtn.disabled = false;
|
|
1054
|
+
saveBtn.textContent = t("meta.save");
|
|
1055
|
+
statusEl.textContent = t("meta.save.err", { msg: e.message });
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
footer.appendChild(statusEl);
|
|
1060
|
+
footer.appendChild(cancelBtn);
|
|
1061
|
+
footer.appendChild(saveBtn);
|
|
1062
|
+
modal.appendChild(footer);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// Standalone README + screenshot editor modal, reachable from the published
|
|
1066
|
+
// extension list at any time (not just after first publish).
|
|
1067
|
+
|
|
1068
|
+
// Prompt user to fill in README before publishing.
|
|
1069
|
+
// Returns a Promise that resolves when the user is ready to continue.
|
|
1070
|
+
// If readme is missing, shows a dialog: "Add description" opens the editor,
|
|
1071
|
+
// "Skip" proceeds without filling in.
|
|
1072
|
+
// Options:
|
|
1073
|
+
// onSaved(readme) — called after user saves the readme in the editor,
|
|
1074
|
+
// use this to trigger a UI refresh in the calling panel.
|
|
1075
|
+
function confirmReadme(ext, { onSaved } = {}) {
|
|
1076
|
+
if (ext.readme && ext.readme.trim()) return Promise.resolve();
|
|
1077
|
+
return new Promise((resolve) => {
|
|
1078
|
+
const overlay = el("div", { class: "studio-modal-overlay" });
|
|
1079
|
+
const box = el("div", { class: "studio-modal", style: "max-width:420px;padding:24px 28px 20px" });
|
|
1080
|
+
box.appendChild(el("h3", { class: "studio-modal-title", style: "margin-bottom:10px", text: t("pub.readme.section") }));
|
|
1081
|
+
box.appendChild(el("p", { style: "margin-bottom:20px;color:var(--color-text-secondary);font-size:14px", text: t("pub.readme.required") }));
|
|
1082
|
+
const footer = el("div", { class: "studio-modal-footer" });
|
|
1083
|
+
const skipBtn = el("button", { class: "studio-btn studio-btn-ghost", text: t("pub.readme.required.skip") });
|
|
1084
|
+
const editBtn = el("button", { class: "studio-btn studio-btn-primary", text: t("pub.readme.required.edit") });
|
|
1085
|
+
function close(goEdit) {
|
|
1086
|
+
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
|
1087
|
+
if (goEdit) {
|
|
1088
|
+
openReadmeEditModal(ext.id, ext.readme || "", (saved) => {
|
|
1089
|
+
ext.readme = saved;
|
|
1090
|
+
if (onSaved) onSaved(saved);
|
|
1091
|
+
resolve();
|
|
1092
|
+
});
|
|
1093
|
+
} else {
|
|
1094
|
+
resolve();
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
skipBtn.addEventListener("click", () => close(false));
|
|
1098
|
+
editBtn.addEventListener("click", () => close(true));
|
|
1099
|
+
footer.appendChild(skipBtn);
|
|
1100
|
+
footer.appendChild(editBtn);
|
|
1101
|
+
box.appendChild(footer);
|
|
1102
|
+
overlay.appendChild(box);
|
|
1103
|
+
document.body.appendChild(overlay);
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function openReadmeEditModal(extId, initialReadme, onSaved) {
|
|
1108
|
+
// Use a wider full-overlay modal with a split editor/preview layout.
|
|
1109
|
+
const overlay = el("div", { class: "studio-modal-overlay studio-readme-overlay" });
|
|
1110
|
+
const box = el("div", { class: "studio-modal studio-readme-modal" });
|
|
1111
|
+
overlay.appendChild(box);
|
|
1112
|
+
document.body.appendChild(overlay);
|
|
1113
|
+
function close() { if (overlay.parentNode) overlay.parentNode.removeChild(overlay); }
|
|
1114
|
+
|
|
1115
|
+
box.appendChild(el("h3", { class: "studio-modal-title", text: t("pub.readme.edit.title") }));
|
|
1116
|
+
|
|
1117
|
+
// Split pane: left = textarea, right = rendered preview
|
|
1118
|
+
const splitPane = el("div", { class: "studio-readme-split" });
|
|
1119
|
+
|
|
1120
|
+
const editorPane = el("div", { class: "studio-readme-pane" });
|
|
1121
|
+
editorPane.appendChild(el("div", { class: "studio-readme-pane-label", text: t("readme.pane.editor") }));
|
|
1122
|
+
const readmeArea = el("textarea", {
|
|
1123
|
+
class: "studio-textarea studio-readme-textarea",
|
|
1124
|
+
placeholder: t("pub.readme.placeholder"),
|
|
1125
|
+
});
|
|
1126
|
+
// Pre-fill with existing readme content
|
|
1127
|
+
if (initialReadme) {
|
|
1128
|
+
readmeArea.value = initialReadme;
|
|
1129
|
+
}
|
|
1130
|
+
editorPane.appendChild(readmeArea);
|
|
1131
|
+
splitPane.appendChild(editorPane);
|
|
1132
|
+
|
|
1133
|
+
const previewPane = el("div", { class: "studio-readme-pane" });
|
|
1134
|
+
previewPane.appendChild(el("div", { class: "studio-readme-pane-label", text: t("readme.pane.preview") }));
|
|
1135
|
+
const previewBody = el("div", { class: "studio-readme-preview extension-readme-body" });
|
|
1136
|
+
previewPane.appendChild(previewBody);
|
|
1137
|
+
splitPane.appendChild(previewPane);
|
|
1138
|
+
|
|
1139
|
+
box.appendChild(splitPane);
|
|
1140
|
+
|
|
1141
|
+
// Render helper
|
|
1142
|
+
function renderPreview() {
|
|
1143
|
+
const md = readmeArea.value;
|
|
1144
|
+
previewBody.innerHTML = md && typeof marked !== "undefined"
|
|
1145
|
+
? marked.parse(md, { breaks: true, gfm: true })
|
|
1146
|
+
: (md ? `<pre>${md}</pre>` : "");
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// Live preview on input
|
|
1150
|
+
readmeArea.addEventListener("input", renderPreview);
|
|
1151
|
+
|
|
1152
|
+
// Initial preview if there's pre-filled content
|
|
1153
|
+
if (readmeArea.value) renderPreview();
|
|
1154
|
+
|
|
1155
|
+
// Screenshots section
|
|
1156
|
+
const screenshotsSection = el("div", { class: "studio-readme-screenshots" });
|
|
1157
|
+
|
|
1158
|
+
const screenshotRow = el("div", { class: "studio-readme-screenshots-row" });
|
|
1159
|
+
|
|
1160
|
+
const fileInput = el("input", { type: "file", accept: "image/png,image/jpeg,image/gif", style: "display:none" });
|
|
1161
|
+
screenshotsSection.appendChild(fileInput);
|
|
1162
|
+
|
|
1163
|
+
const uploadBtn = el("button", { class: "studio-btn studio-btn-ghost studio-btn-sm", text: t("pub.screenshot.upload") });
|
|
1164
|
+
uploadBtn.addEventListener("click", () => fileInput.click());
|
|
1165
|
+
screenshotRow.appendChild(uploadBtn);
|
|
1166
|
+
screenshotRow.appendChild(el("span", { class: "studio-readme-screenshots-hint", text: t("pub.screenshot.hint") }));
|
|
1167
|
+
screenshotsSection.appendChild(screenshotRow);
|
|
1168
|
+
|
|
1169
|
+
const screenshotErr = el("p", { class: "studio-modal-status studio-modal-status-error", style: "display:none;margin-top:6px" });
|
|
1170
|
+
screenshotsSection.appendChild(screenshotErr);
|
|
1171
|
+
fileInput.addEventListener("change", async () => {
|
|
1172
|
+
const file = fileInput.files[0];
|
|
1173
|
+
if (!file) return;
|
|
1174
|
+
fileInput.value = "";
|
|
1175
|
+
screenshotErr.style.display = "none";
|
|
1176
|
+
if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) {
|
|
1177
|
+
screenshotErr.textContent = t("pub.screenshot.err", { msg: t("pub.screenshot.err.type") });
|
|
1178
|
+
screenshotErr.style.display = "";
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
if (file.size > 5 * 1024 * 1024) {
|
|
1182
|
+
screenshotErr.textContent = t("pub.screenshot.err", { msg: t("pub.screenshot.err.size") });
|
|
1183
|
+
screenshotErr.style.display = "";
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
uploadBtn.disabled = true;
|
|
1187
|
+
uploadBtn.textContent = t("pub.screenshot.uploading");
|
|
1188
|
+
screenshotErr.style.display = "none";
|
|
1189
|
+
try {
|
|
1190
|
+
const form = new FormData();
|
|
1191
|
+
form.append("ext_id", extId);
|
|
1192
|
+
form.append("file", file);
|
|
1193
|
+
const res = await fetch("/api/ext/ext-studio/screenshot", { method: "POST", body: form });
|
|
1194
|
+
const json = await res.json();
|
|
1195
|
+
if (!res.ok || !json.ok) throw new Error(json.error || "upload failed");
|
|
1196
|
+
// Insert  at cursor position in the editor.
|
|
1197
|
+
// Ensure the image tag is on its own line by adding newlines if needed.
|
|
1198
|
+
const mdImg = ``;
|
|
1199
|
+
const start = readmeArea.selectionStart;
|
|
1200
|
+
const end = readmeArea.selectionEnd;
|
|
1201
|
+
const before = readmeArea.value.slice(0, start);
|
|
1202
|
+
const after = readmeArea.value.slice(end);
|
|
1203
|
+
const prefix = before.length > 0 && !before.endsWith("\n") ? "\n" : "";
|
|
1204
|
+
const suffix = after.length > 0 && !after.startsWith("\n") ? "\n" : "";
|
|
1205
|
+
const inserted = prefix + mdImg + suffix;
|
|
1206
|
+
readmeArea.value = before + inserted + after;
|
|
1207
|
+
readmeArea.selectionStart = readmeArea.selectionEnd = start + inserted.length;
|
|
1208
|
+
readmeArea.focus();
|
|
1209
|
+
readmeArea.dispatchEvent(new Event("input"));
|
|
1210
|
+
} catch (e) {
|
|
1211
|
+
screenshotErr.textContent = t("pub.screenshot.err", { msg: e.message });
|
|
1212
|
+
screenshotErr.style.display = "";
|
|
1213
|
+
} finally {
|
|
1214
|
+
uploadBtn.disabled = false;
|
|
1215
|
+
uploadBtn.textContent = t("pub.screenshot.upload");
|
|
1216
|
+
}
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
box.appendChild(screenshotsSection);
|
|
1220
|
+
|
|
1221
|
+
const statusEl = el("p", { class: "studio-modal-status", style: "display:none;margin-top:8px" });
|
|
1222
|
+
box.appendChild(statusEl);
|
|
1223
|
+
|
|
1224
|
+
const footer = el("div", { class: "studio-modal-footer" });
|
|
1225
|
+
const cancelBtn = el("button", { class: "studio-btn studio-btn-ghost", text: t("pub.btn.cancel") });
|
|
1226
|
+
cancelBtn.addEventListener("click", close);
|
|
1227
|
+
const saveBtn = el("button", { class: "studio-btn studio-btn-primary", text: t("pub.readme.save") });
|
|
1228
|
+
saveBtn.addEventListener("click", async () => {
|
|
1229
|
+
const readme = readmeArea.value.trim();
|
|
1230
|
+
if (!readme) {
|
|
1231
|
+
statusEl.className = "studio-modal-status studio-modal-status-error";
|
|
1232
|
+
statusEl.textContent = t("pub.readme.save.empty");
|
|
1233
|
+
statusEl.style.display = "";
|
|
1234
|
+
readmeArea.focus();
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
saveBtn.disabled = true;
|
|
1238
|
+
saveBtn.textContent = t("pub.readme.saving");
|
|
1239
|
+
cancelBtn.disabled = true;
|
|
1240
|
+
statusEl.style.display = "none";
|
|
1241
|
+
try {
|
|
1242
|
+
await patchJson("/readme", { ext_id: extId, readme });
|
|
1243
|
+
close();
|
|
1244
|
+
if (onSaved) onSaved(readme);
|
|
1245
|
+
} catch (e) {
|
|
1246
|
+
statusEl.textContent = t("err.generic", { msg: e.message });
|
|
1247
|
+
statusEl.style.display = "";
|
|
1248
|
+
saveBtn.disabled = false;
|
|
1249
|
+
saveBtn.textContent = t("pub.readme.save");
|
|
1250
|
+
cancelBtn.disabled = false;
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
1253
|
+
footer.appendChild(cancelBtn);
|
|
1254
|
+
footer.appendChild(saveBtn);
|
|
1255
|
+
box.appendChild(footer);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
780
1258
|
function createPublishPanel() {
|
|
781
1259
|
let container = null;
|
|
782
1260
|
let unsub = null;
|
|
@@ -789,13 +1267,14 @@
|
|
|
789
1267
|
async function doPublish() {
|
|
790
1268
|
const ext = store.selected();
|
|
791
1269
|
if (!ext) return;
|
|
1270
|
+
await confirmReadme(ext, { onSaved: () => rebuild() });
|
|
792
1271
|
const prevVersionPromise = getJson("/published")
|
|
793
1272
|
.then((data) => {
|
|
794
1273
|
const match = (data.extensions || []).find((e) => e.id === ext.id);
|
|
795
1274
|
return match ? match.version : null;
|
|
796
1275
|
})
|
|
797
1276
|
.catch(() => null);
|
|
798
|
-
const ok = await runPublishFlow(ext, prevVersionPromise);
|
|
1277
|
+
const ok = await runPublishFlow(ext, prevVersionPromise, { onPublishDone: loadPublished });
|
|
799
1278
|
if (ok) loadPublished();
|
|
800
1279
|
}
|
|
801
1280
|
|
|
@@ -814,13 +1293,27 @@
|
|
|
814
1293
|
}
|
|
815
1294
|
}
|
|
816
1295
|
|
|
1296
|
+
function renderPublishedSkeleton(box) {
|
|
1297
|
+
const card = el("div", { class: "studio-skill-card studio-skeleton-card" });
|
|
1298
|
+
const head = el("div", { class: "studio-skill-head" });
|
|
1299
|
+
head.appendChild(el("span", { class: "studio-skeleton studio-skeleton-name" }));
|
|
1300
|
+
head.appendChild(el("span", { class: "studio-skeleton studio-skeleton-badge" }));
|
|
1301
|
+
head.appendChild(el("span", { class: "studio-skeleton studio-skeleton-ver" }));
|
|
1302
|
+
head.appendChild(el("span", { class: "studio-skeleton studio-skeleton-btn" }));
|
|
1303
|
+
card.appendChild(head);
|
|
1304
|
+
box.appendChild(card);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
817
1307
|
async function loadPublished() {
|
|
818
1308
|
const box = container.querySelector(".studio-published");
|
|
819
1309
|
if (!box) return;
|
|
820
1310
|
box.innerHTML = "";
|
|
821
1311
|
box.appendChild(el("div", { class: "studio-label", text: t("published.title") }));
|
|
1312
|
+
renderPublishedSkeleton(box);
|
|
822
1313
|
try {
|
|
823
1314
|
const data = await getJson("/published");
|
|
1315
|
+
box.innerHTML = "";
|
|
1316
|
+
box.appendChild(el("div", { class: "studio-label", text: t("published.title") }));
|
|
824
1317
|
const exts = data.extensions || [];
|
|
825
1318
|
if (!exts.length) { box.appendChild(el("p", { class: "studio-empty", text: t("published.empty") })); return; }
|
|
826
1319
|
exts.forEach((e) => {
|
|
@@ -830,7 +1323,7 @@
|
|
|
830
1323
|
const statusKind = e.status === "draft" ? "local" : "published";
|
|
831
1324
|
head.appendChild(el("span", { class: `studio-skill-badge studio-skill-badge-${statusKind}`, text: e.status === "draft" ? t("extlist.badge.draft") : t("extlist.badge.published") }));
|
|
832
1325
|
if (e.version) head.appendChild(el("span", { style: "font-size:11px;color:var(--color-text-muted);", text: "v" + e.version }));
|
|
833
|
-
const unpubBtn = el("button", { class: "studio-btn studio-btn-danger", text: t("btn.unpublish") });
|
|
1326
|
+
const unpubBtn = el("button", { class: "studio-btn studio-btn-danger studio-btn-sm", text: t("btn.unpublish") });
|
|
834
1327
|
unpubBtn.style.marginLeft = "auto";
|
|
835
1328
|
unpubBtn.addEventListener("click", async () => {
|
|
836
1329
|
if (!window.confirm(t("published.confirm", { id: e.id }))) return;
|
|
@@ -843,6 +1336,8 @@
|
|
|
843
1336
|
box.appendChild(card);
|
|
844
1337
|
});
|
|
845
1338
|
} catch (e) {
|
|
1339
|
+
box.innerHTML = "";
|
|
1340
|
+
box.appendChild(el("div", { class: "studio-label", text: t("published.title") }));
|
|
846
1341
|
box.appendChild(el("p", { class: "studio-empty", text: t("err.generic", { msg: e.message }) }));
|
|
847
1342
|
}
|
|
848
1343
|
}
|
|
@@ -852,7 +1347,19 @@
|
|
|
852
1347
|
container.innerHTML = "";
|
|
853
1348
|
container.appendChild(renderPicker(rebuild));
|
|
854
1349
|
|
|
855
|
-
|
|
1350
|
+
const ext = store.selected();
|
|
1351
|
+
if (!ext) return;
|
|
1352
|
+
|
|
1353
|
+
container.appendChild(buildExtInfoCard(
|
|
1354
|
+
ext,
|
|
1355
|
+
() => openMetaEditModal(ext, () => store.reload().then(rebuild))
|
|
1356
|
+
));
|
|
1357
|
+
|
|
1358
|
+
container.appendChild(buildReadmeCard(
|
|
1359
|
+
ext.id,
|
|
1360
|
+
ext.readme,
|
|
1361
|
+
() => openReadmeEditModal(ext.id, ext.readme, () => store.reload().then(rebuild))
|
|
1362
|
+
));
|
|
856
1363
|
|
|
857
1364
|
const bar = el("div", { class: "studio-actions" });
|
|
858
1365
|
bar.appendChild(el("button", { class: "studio-btn studio-btn-primary studio-publish-btn", text: t("btn.publish"), onclick: doPublish }));
|
|
@@ -928,6 +1435,15 @@
|
|
|
928
1435
|
return el("span", { class: "studio-skill-badge studio-skill-badge-" + kind, text });
|
|
929
1436
|
}
|
|
930
1437
|
|
|
1438
|
+
function formatUnitCounts(counts) {
|
|
1439
|
+
if (!counts || typeof counts !== "object") return "";
|
|
1440
|
+
return Object.keys(counts).map((kind) => {
|
|
1441
|
+
const n = parseInt(counts[kind], 10);
|
|
1442
|
+
if (!n) return null;
|
|
1443
|
+
return `${n} ${t("unit." + kind + (n > 1 ? "s" : ""))}`;
|
|
1444
|
+
}).filter(Boolean).join(" · ");
|
|
1445
|
+
}
|
|
1446
|
+
|
|
931
1447
|
function cloudCard(ext) {
|
|
932
1448
|
const card = el("div", { class: "studio-skill-card" });
|
|
933
1449
|
const head = el("div", { class: "studio-skill-head" });
|
|
@@ -969,13 +1485,14 @@
|
|
|
969
1485
|
|
|
970
1486
|
const meta = el("div", { class: "studio-skill-meta" });
|
|
971
1487
|
if (ext.version) meta.appendChild(el("span", { text: "v" + ext.version }));
|
|
972
|
-
|
|
1488
|
+
const unitsText = formatUnitCounts(ext.unit_counts);
|
|
1489
|
+
if (unitsText) meta.appendChild(el("span", { text: unitsText }));
|
|
973
1490
|
card.appendChild(meta);
|
|
974
1491
|
|
|
975
1492
|
const actions = el("div", { class: "studio-actions" });
|
|
976
1493
|
const pub = el("button", { class: "studio-btn studio-btn-primary", text: published ? t("extlist.btn.update") : t("extlist.btn.publish") });
|
|
977
|
-
pub.disabled =
|
|
978
|
-
pub.title =
|
|
1494
|
+
pub.disabled = !!ext.error_count;
|
|
1495
|
+
pub.title = ext.error_count ? t("extlist.verify.errors", { n: ext.error_count }) : "";
|
|
979
1496
|
pub.addEventListener("click", () => doPublish(ext, cloudEntry ? cloudEntry.version : null));
|
|
980
1497
|
actions.appendChild(pub);
|
|
981
1498
|
|
|
@@ -1025,7 +1542,9 @@
|
|
|
1025
1542
|
}
|
|
1026
1543
|
|
|
1027
1544
|
async function doPublish(ext, prevVersion) {
|
|
1028
|
-
|
|
1545
|
+
await confirmReadme(ext, { onSaved: () => { reload().then(rebuild); } });
|
|
1546
|
+
const onDone = async () => { await reload(); rebuild(); };
|
|
1547
|
+
const ok = await runPublishFlow(ext, prevVersion, { onPublishDone: onDone });
|
|
1029
1548
|
if (ok) { await reload(); rebuild(); }
|
|
1030
1549
|
}
|
|
1031
1550
|
|
|
@@ -1333,18 +1852,16 @@
|
|
|
1333
1852
|
.studio-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
|
|
1334
1853
|
.studio-label { font-size: 12px; font-weight: 500; color: var(--color-text-secondary); }
|
|
1335
1854
|
.studio-select, .studio-textarea { width: 100%; box-sizing: border-box; background: var(--color-bg-input); border: 1px solid var(--color-border-primary); border-radius: var(--radius-sm); padding: 7px 8px; color: var(--color-text-primary); font-size: 13px; font-family: inherit; }
|
|
1855
|
+
.studio-select { appearance: none; -webkit-appearance: none; padding-right: 28px; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2 4l4 4 4-4' stroke='%23888' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 8px center; }
|
|
1336
1856
|
.studio-select:focus, .studio-textarea:focus { border-color: var(--color-accent-primary); outline: none; }
|
|
1337
1857
|
.studio-empty { color: var(--color-text-muted); font-size: 12px; margin: 4px 0; }
|
|
1338
1858
|
.studio-hint { color: var(--color-text-tertiary); font-size: 12px; line-height: 1.5; margin: 10px 0 0; }
|
|
1339
1859
|
.studio-detail { border: 1px solid var(--color-border-primary); border-radius: var(--radius-sm); padding: 12px; margin-bottom: 14px; background: var(--color-bg-secondary); }
|
|
1340
|
-
.studio-detail-name { margin: 0
|
|
1341
|
-
.studio-detail-desc { margin: 0 0
|
|
1342
|
-
.studio-meta { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; margin: 0 0 10px; font-size: 12px; }
|
|
1343
|
-
.studio-meta dt { color: var(--color-text-tertiary); }
|
|
1344
|
-
.studio-meta dd { margin: 0; color: var(--color-text-primary); }
|
|
1860
|
+
.studio-detail-name { margin: 0; font-size: 14px; color: var(--color-text-primary); }
|
|
1861
|
+
.studio-detail-desc { margin: 4px 0 0; font-size: 12px; color: var(--color-text-secondary); line-height: 1.5; }
|
|
1345
1862
|
.studio-units { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
1346
1863
|
.studio-unit-chip { display: inline-flex; background: var(--color-bg-hover); color: var(--color-text-primary); border: 1px solid var(--color-border-primary); border-radius: var(--radius-sm); padding: 2px 8px; font-size: 12px; }
|
|
1347
|
-
.studio-actions { display: flex; align-items: center; gap: 8px; margin: 12px 0; }
|
|
1864
|
+
.studio-actions { display: flex; align-items: center; gap: 8px; margin: 12px 0 0; }
|
|
1348
1865
|
.studio-skill-promo .studio-actions { margin: 12px 0 0; }
|
|
1349
1866
|
.studio-btn { padding: 7px 14px; border-radius: var(--radius-sm); border: 1px solid var(--color-border-primary); background: transparent; color: var(--color-text-secondary); cursor: pointer; font-size: 13px; font-weight: 500; }
|
|
1350
1867
|
.studio-btn:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
|
|
@@ -1373,6 +1890,12 @@
|
|
|
1373
1890
|
.studio-feedback-error { color: var(--color-error); }
|
|
1374
1891
|
.studio-feedback-warn { color: var(--color-warning, var(--color-text-secondary)); }
|
|
1375
1892
|
.studio-published { margin-top: 16px; border-top: 1px solid var(--color-border-primary); padding-top: 12px; }
|
|
1893
|
+
@keyframes studio-shimmer { 0% { background-position: -200px 0; } 100% { background-position: calc(200px + 100%) 0; } }
|
|
1894
|
+
.studio-skeleton { display: inline-block; border-radius: 4px; background: linear-gradient(90deg, var(--color-bg-hover) 25%, var(--color-border-primary) 50%, var(--color-bg-hover) 75%); background-size: 200px 100%; animation: studio-shimmer 1.4s infinite; }
|
|
1895
|
+
.studio-skeleton-name { width: 120px; height: 14px; }
|
|
1896
|
+
.studio-skeleton-badge { width: 44px; height: 16px; border-radius: 10px; }
|
|
1897
|
+
.studio-skeleton-ver { width: 36px; height: 12px; }
|
|
1898
|
+
.studio-skeleton-btn { width: 44px; height: 26px; margin-left: auto; border-radius: 6px; }
|
|
1376
1899
|
.studio-published-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px 0; font-size: 12px; border-bottom: 1px solid var(--color-border-primary); }
|
|
1377
1900
|
.studio-published-name { display: flex; align-items: center; gap: 6px; color: var(--color-text-primary); }
|
|
1378
1901
|
.studio-page { width: 100%; }
|
|
@@ -1395,8 +1918,8 @@
|
|
|
1395
1918
|
.studio-skill-badge-local { background: var(--color-bg-hover); color: var(--color-text-tertiary); }
|
|
1396
1919
|
.studio-skill-badge-changed { background: var(--color-warning-bg, var(--color-bg-hover)); color: var(--color-warning, var(--color-text-secondary)); }
|
|
1397
1920
|
.studio-skill-badge-shadow { background: var(--color-bg-hover); color: var(--color-text-secondary); }
|
|
1398
|
-
.studio-skill-desc { margin: 6px 0
|
|
1399
|
-
.studio-skill-meta { display: flex; gap: 12px; font-size: 11px; color: var(--color-text-muted); }
|
|
1921
|
+
.studio-skill-desc { margin: 6px 0 6px; font-size: 12px; color: var(--color-text-secondary); line-height: 1.5; }
|
|
1922
|
+
.studio-skill-meta { display: flex; gap: 12px; font-size: 11px; color: var(--color-text-muted); margin-top: 6px; }
|
|
1400
1923
|
.studio-skill-promo { border: 1px solid var(--color-border-primary); border-radius: var(--radius-md, 8px); padding: 16px 18px; margin: 0 0 24px; background: var(--color-bg-secondary); }
|
|
1401
1924
|
.studio-skill-promo-text { margin: 0 0 4px; font-size: 14px; font-weight: 600; color: var(--color-text-primary); }
|
|
1402
1925
|
.studio-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 9999; animation: studio-overlay-in 0.15s ease; }
|
|
@@ -1419,6 +1942,33 @@
|
|
|
1419
1942
|
.studio-modal-link { display: inline-block; font-size: 13px; color: var(--color-accent-primary); }
|
|
1420
1943
|
.studio-modal-footer { display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--color-border-primary); }
|
|
1421
1944
|
.studio-pub-success { margin-right: auto; font-size: 13px; font-weight: 500; color: var(--color-success); }
|
|
1945
|
+
.studio-section { margin-bottom: 20px; }
|
|
1946
|
+
.studio-detail-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
|
|
1947
|
+
.studio-info-row { display: flex; align-items: baseline; gap: 8px; margin: 8px 0 0; }
|
|
1948
|
+
.studio-info-label { font-size: 12px; color: var(--color-text-tertiary); flex-shrink: 0; }
|
|
1949
|
+
.studio-info-value { font-size: 13px; color: var(--color-text-primary); }
|
|
1950
|
+
.studio-section-label { margin-top: 10px; margin-bottom: 6px; }
|
|
1951
|
+
.studio-detail-head .studio-detail-name { margin: 0; }
|
|
1952
|
+
.studio-form-status { font-size: 12px; color: var(--color-text-tertiary); }
|
|
1953
|
+
.studio-btn-sm { padding: 4px 10px; font-size: 12px; }
|
|
1954
|
+
.studio-readme-overlay { align-items: center; justify-content: center; }
|
|
1955
|
+
.studio-readme-modal { width: min(1200px, calc(100vw - 48px)); height: calc(100vh - 80px); max-width: calc(100vw - 48px); border-radius: var(--radius-md, 8px); margin: 0; padding: 24px 28px 20px; display: flex; flex-direction: column; }
|
|
1956
|
+
.studio-readme-modal .studio-modal-title { margin-bottom: 16px; flex-shrink: 0; }
|
|
1957
|
+
.studio-readme-split { display: flex; gap: 14px; flex: 1; min-height: 0; margin-bottom: 18px; }
|
|
1958
|
+
.studio-readme-pane { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
1959
|
+
.studio-readme-pane-label { font-size: 12px; font-weight: 600; color: var(--color-text-muted); letter-spacing: 0.02em; padding-bottom: 6px; border-bottom: 1px solid var(--color-border-primary); margin-bottom: 8px; }
|
|
1960
|
+
.studio-readme-textarea { flex: 1; resize: none; font-family: monospace; font-size: 12.5px; line-height: 1.65; border-radius: var(--radius-sm); }
|
|
1961
|
+
.studio-readme-preview { flex: 1; overflow-y: auto; overflow-x: hidden; border: 1px solid var(--color-border-primary); border-radius: var(--radius-sm); padding: 12px 16px; background: var(--color-bg-secondary); font-size: 13px; line-height: 1.75; }
|
|
1962
|
+
.studio-readme-preview > *:first-child { margin-top: 0; }
|
|
1963
|
+
.studio-readme-preview img { max-width: 100%; height: auto; display: block; border-radius: var(--radius-sm); }
|
|
1964
|
+
.studio-readme-screenshots { border-top: 1px solid var(--color-border-primary); padding-top: 14px; margin-bottom: 6px; }
|
|
1965
|
+
.studio-readme-screenshots-row { display: flex; align-items: center; gap: 10px; }
|
|
1966
|
+
.studio-readme-screenshots-hint { font-size: 11px; color: var(--color-text-muted); }
|
|
1967
|
+
.studio-readme-card { margin-top: 8px; }
|
|
1968
|
+
.studio-readme-card-preview { max-height: 200px; overflow-y: auto; font-size: 13px; }
|
|
1969
|
+
.extension-readme-body img { max-width: min(800px, 100%); height: auto; }
|
|
1970
|
+
.studio-link-btn { background: none; border: none; padding: 0; font-size: 12px; color: var(--color-accent-primary); cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
|
|
1971
|
+
.studio-link-btn:hover { color: var(--color-accent-hover); }
|
|
1422
1972
|
`;
|
|
1423
1973
|
document.head.appendChild(style);
|
|
1424
1974
|
})();
|