openclacky 1.3.6 → 1.3.7
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 +30 -0
- data/lib/clacky/agent.rb +8 -1
- data/lib/clacky/agent_profile.rb +121 -53
- data/lib/clacky/brand_config.rb +309 -6
- data/lib/clacky/cli.rb +4 -57
- data/lib/clacky/default_extensions/coding/ext.yml +18 -0
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +95 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +254 -0
- data/lib/clacky/default_extensions/ext-studio/ext.yml +32 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +1269 -0
- data/lib/clacky/default_extensions/ext-studio/skills/ext-debug/SKILL.md +71 -0
- data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +74 -0
- data/lib/clacky/default_extensions/ext-studio/skills/ext-scaffold/SKILL.md +65 -0
- data/lib/clacky/default_extensions/general/ext.yml +18 -0
- data/lib/clacky/default_extensions/git/ext.yml +12 -0
- data/lib/clacky/{default_agents/_panels/git/panel.js → default_extensions/git/panels/git/view.js} +1 -2
- data/lib/clacky/default_extensions/meeting/{handler.rb → api/handler.rb} +78 -26
- data/lib/clacky/default_extensions/meeting/ext.yml +15 -0
- data/lib/clacky/default_extensions/meeting/{meeting.js → panels/meeting/view.js} +266 -89
- data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +2 -1
- data/lib/clacky/default_extensions/time_machine/ext.yml +12 -0
- data/lib/clacky/{default_agents/_panels/time_machine/panel.js → default_extensions/time_machine/panels/time_machine/view.js} +58 -47
- data/lib/clacky/default_skills/channel-manager/discord_setup.rb +1 -1
- data/lib/clacky/default_skills/extend-openclacky/SKILL.md +83 -18
- data/lib/clacky/{api_extension.rb → extension/api_extension.rb} +27 -11
- data/lib/clacky/extension/api_loader.rb +136 -0
- data/lib/clacky/extension/cli_commands.rb +226 -0
- data/lib/clacky/{server/api_extension_dispatcher.rb → extension/dispatcher.rb} +22 -9
- data/lib/clacky/extension/hook_loader.rb +77 -0
- data/lib/clacky/extension/loader.rb +483 -0
- data/lib/clacky/extension/packager.rb +226 -0
- data/lib/clacky/{patch_loader.rb → extension/patch_loader.rb} +45 -0
- data/lib/clacky/extension/scaffold/template_renderer.rb +64 -0
- data/lib/clacky/extension/scaffold/templates/full/README.md.erb +48 -0
- data/lib/clacky/extension/scaffold/templates/full/agents/designer.md +5 -0
- data/lib/clacky/extension/scaffold/templates/full/api/handler.rb.erb +16 -0
- data/lib/clacky/extension/scaffold/templates/full/channels/noop.rb.erb +40 -0
- data/lib/clacky/extension/scaffold/templates/full/ext.yml.erb +41 -0
- data/lib/clacky/extension/scaffold/templates/full/hooks/audit.rb +9 -0
- data/lib/clacky/extension/scaffold/templates/full/panels/dashboard/view.js.erb +194 -0
- data/lib/clacky/extension/scaffold/templates/full/patches/audit.rb +15 -0
- data/lib/clacky/extension/scaffold/templates/full/skills/__slug__-skill/SKILL.md.erb +8 -0
- data/lib/clacky/extension/scaffold/templates/hello/api/handler.rb.erb +9 -0
- data/lib/clacky/extension/scaffold/templates/hello/ext.yml.erb +12 -0
- data/lib/clacky/extension/scaffold/templates/hello/panels/hello/view.js.erb +31 -0
- data/lib/clacky/extension/scaffold.rb +55 -0
- data/lib/clacky/extension/verifier.rb +196 -0
- data/lib/clacky/identity.rb +70 -0
- data/lib/clacky/locales/en.rb +19 -1
- data/lib/clacky/locales/zh.rb +19 -1
- data/lib/clacky/platform_http_client.rb +18 -15
- data/lib/clacky/server/channel/extension_adapter_loader.rb +32 -0
- data/lib/clacky/server/channel.rb +6 -0
- data/lib/clacky/server/http_server.rb +313 -144
- data/lib/clacky/server/session_registry.rb +9 -2
- data/lib/clacky/skill_loader.rb +28 -1
- data/lib/clacky/ui2/ui_controller.rb +9 -6
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +748 -573
- data/lib/clacky/web/app.js +63 -15
- data/lib/clacky/web/auth.js +2 -0
- data/lib/clacky/web/components/notify.js +2 -0
- data/lib/clacky/web/components/sidebar.js +1 -5
- data/lib/clacky/web/core/ext.js +316 -79
- data/lib/clacky/web/features/backup/store.js +1 -0
- data/lib/clacky/web/features/backup/view.js +0 -1
- data/lib/clacky/web/features/brand/view.js +13 -5
- data/lib/clacky/web/features/extensions/store.js +184 -0
- data/lib/clacky/web/features/extensions/view.js +389 -0
- data/lib/clacky/web/features/new-session/store.js +186 -0
- data/lib/clacky/web/features/new-session/view.js +319 -0
- data/lib/clacky/web/features/share/view.js +63 -14
- data/lib/clacky/web/features/skills/store.js +3 -1
- data/lib/clacky/web/features/skills/view.js +1 -1
- data/lib/clacky/web/features/tasks/view.js +12 -0
- data/lib/clacky/web/features/trash/store.js +24 -6
- data/lib/clacky/web/features/trash/view.js +46 -9
- data/lib/clacky/web/features/workspace/store.js +2 -0
- data/lib/clacky/web/features/workspace/view.js +0 -1
- data/lib/clacky/web/i18n.js +118 -106
- data/lib/clacky/web/index.html +138 -188
- data/lib/clacky/web/sessions.js +64 -269
- data/lib/clacky/web/settings.js +1 -1
- data/lib/clacky/web/skills.js +2 -0
- data/lib/clacky/web/theme.js +2 -0
- data/lib/clacky/web/ws-dispatcher.js +1 -0
- data/lib/clacky/web/ws.js +2 -0
- data/lib/clacky.rb +21 -10
- metadata +55 -24
- data/benchmark/runner.rb +0 -441
- data/lib/clacky/api_extension_loader.rb +0 -168
- data/lib/clacky/default_agents/SOUL.md +0 -3
- data/lib/clacky/default_agents/USER.md +0 -1
- data/lib/clacky/default_agents/coding/profile.yml +0 -5
- data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
- data/lib/clacky/default_agents/general/profile.yml +0 -2
- data/lib/clacky/default_extensions/meeting/meta.yml +0 -3
- data/lib/clacky/web/features/creator/store.js +0 -81
- data/lib/clacky/web/features/creator/view.js +0 -380
- /data/lib/clacky/{default_agents → default_extensions/coding/agents}/coding/system_prompt.md +0 -0
- /data/lib/clacky/{default_agents → default_extensions/general/agents}/general/system_prompt.md +0 -0
- /data/lib/clacky/{default_agents/base_prompt.md → prompts/base.md} +0 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// ── Extensions · store — data, state, network for the extension marketplace ─
|
|
2
|
+
//
|
|
3
|
+
// The store is the single source of truth for the public extension catalog. It
|
|
4
|
+
// owns state, talks to the local server (which proxies the platform's public
|
|
5
|
+
// /api/v1/extensions endpoint), and emits events so the view re-renders. It
|
|
6
|
+
// NEVER touches the DOM directly.
|
|
7
|
+
//
|
|
8
|
+
// Extension archives are NOT downloadable from here — extensions ship inside
|
|
9
|
+
// license-gated brand packages (path B distribution). This panel is a
|
|
10
|
+
// read-only browse/search catalog of metadata only.
|
|
11
|
+
//
|
|
12
|
+
// Two event channels (same convention as the skills store):
|
|
13
|
+
// 1. Internal bus (Extensions.on / _emit) — always live; the core view
|
|
14
|
+
// subscribes here so the panel keeps rendering under ?pure=true.
|
|
15
|
+
// 2. Clacky.ext.emit(...) — extension bus; silenced in pure mode.
|
|
16
|
+
//
|
|
17
|
+
// Depends on: I18n, global $ / escapeHtml helpers, Clacky.ext (core/ext.js)
|
|
18
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const ExtensionsStore = (() => {
|
|
21
|
+
// ── State (single source of truth) ─────────────────────────────────────
|
|
22
|
+
let _extensions = []; // [{ id, name, name_zh, description, ..., units }]
|
|
23
|
+
let _query = ""; // current search text
|
|
24
|
+
let _sort = "newest"; // "newest" | "updated" | "downloads"
|
|
25
|
+
let _loading = false;
|
|
26
|
+
let _error = null; // soft warning when the store is unreachable
|
|
27
|
+
let _detail = null; // currently opened extension detail, or null
|
|
28
|
+
let _detailLoading = false;
|
|
29
|
+
let _detailError = null;
|
|
30
|
+
|
|
31
|
+
// ── Internal event bus ──────────────────────────────────────────────────
|
|
32
|
+
const _listeners = {}; // event => [handler]
|
|
33
|
+
|
|
34
|
+
function _on(event, handler) {
|
|
35
|
+
(_listeners[event] ||= []).push(handler);
|
|
36
|
+
return () => {
|
|
37
|
+
const list = _listeners[event];
|
|
38
|
+
const i = list ? list.indexOf(handler) : -1;
|
|
39
|
+
if (i >= 0) list.splice(i, 1);
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function _emit(event, payload) {
|
|
44
|
+
(_listeners[event] || []).forEach((h) => h(payload));
|
|
45
|
+
if (window.Clacky && Clacky.ext) Clacky.ext.emit(event, payload);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Read-only accessors used by the view ────────────────────────────────
|
|
49
|
+
const state = {
|
|
50
|
+
get extensions() { return _extensions; },
|
|
51
|
+
get query() { return _query; },
|
|
52
|
+
get sort() { return _sort; },
|
|
53
|
+
get loading() { return _loading; },
|
|
54
|
+
get error() { return _error; },
|
|
55
|
+
get detail() { return _detail; },
|
|
56
|
+
get detailLoading() { return _detailLoading; },
|
|
57
|
+
get detailError() { return _detailError; },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const Extensions = {
|
|
61
|
+
on: _on,
|
|
62
|
+
state,
|
|
63
|
+
|
|
64
|
+
/** Fetch the catalog from the server for the current query + sort. */
|
|
65
|
+
async load() {
|
|
66
|
+
_loading = true;
|
|
67
|
+
_error = null;
|
|
68
|
+
_emit("extensions:loading");
|
|
69
|
+
try {
|
|
70
|
+
const params = new URLSearchParams();
|
|
71
|
+
if (_query) params.set("q", _query);
|
|
72
|
+
if (_sort) params.set("sort", _sort);
|
|
73
|
+
const qs = params.toString();
|
|
74
|
+
const res = await fetch("/api/store/extensions" + (qs ? "?" + qs : ""));
|
|
75
|
+
const data = await res.json();
|
|
76
|
+
_extensions = data.extensions || [];
|
|
77
|
+
_error = data.warning || null;
|
|
78
|
+
_loading = false;
|
|
79
|
+
_emit("extensions:changed", { extensions: _extensions, warning: _error });
|
|
80
|
+
} catch (e) {
|
|
81
|
+
console.error("[Extensions] load failed", e);
|
|
82
|
+
_extensions = [];
|
|
83
|
+
_error = I18n.t("extensions.loadFailed");
|
|
84
|
+
_loading = false;
|
|
85
|
+
_emit("extensions:error", { network: true });
|
|
86
|
+
} finally {
|
|
87
|
+
_loading = false;
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
/** Set the search text and reload. */
|
|
92
|
+
setQuery(query) {
|
|
93
|
+
_query = (query || "").trim();
|
|
94
|
+
return Extensions.load();
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
/** Set the sort order and reload. */
|
|
98
|
+
setSort(sort) {
|
|
99
|
+
_sort = sort || "newest";
|
|
100
|
+
return Extensions.load();
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
/** Open the detail view for one extension (fetches contributes + versions). */
|
|
104
|
+
async loadDetail(id) {
|
|
105
|
+
if (!id) return;
|
|
106
|
+
_detail = null;
|
|
107
|
+
_detailLoading = true;
|
|
108
|
+
_detailError = null;
|
|
109
|
+
_emit("extensions:detail");
|
|
110
|
+
try {
|
|
111
|
+
const res = await fetch("/api/store/extension?id=" + encodeURIComponent(id));
|
|
112
|
+
const data = await res.json();
|
|
113
|
+
if (res.ok && data.ok && data.extension) {
|
|
114
|
+
_detail = data.extension;
|
|
115
|
+
_detailError = null;
|
|
116
|
+
} else {
|
|
117
|
+
_detail = null;
|
|
118
|
+
_detailError = data.error || I18n.t("extensions.loadFailed");
|
|
119
|
+
}
|
|
120
|
+
} catch (e) {
|
|
121
|
+
console.error("[Extensions] loadDetail failed", e);
|
|
122
|
+
_detail = null;
|
|
123
|
+
_detailError = I18n.t("extensions.loadFailed");
|
|
124
|
+
} finally {
|
|
125
|
+
_detailLoading = false;
|
|
126
|
+
_emit("extensions:detail");
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/** Close the detail view. */
|
|
131
|
+
closeDetail() {
|
|
132
|
+
_detail = null;
|
|
133
|
+
_detailLoading = false;
|
|
134
|
+
_detailError = null;
|
|
135
|
+
_emit("extensions:detail");
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
/** Disable/enable an installed extension, then refresh the open detail. */
|
|
139
|
+
async setEnabled(id, enabled) {
|
|
140
|
+
if (!id) return;
|
|
141
|
+
const path = enabled ? "/api/store/extension/enable" : "/api/store/extension/disable";
|
|
142
|
+
try {
|
|
143
|
+
const res = await fetch(path, {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: { "Content-Type": "application/json" },
|
|
146
|
+
body: JSON.stringify({ id }),
|
|
147
|
+
});
|
|
148
|
+
const data = await res.json();
|
|
149
|
+
if (!res.ok || !data.ok) throw new Error(data.error || "toggle failed");
|
|
150
|
+
await Extensions.loadDetail(id);
|
|
151
|
+
Extensions.load();
|
|
152
|
+
} catch (e) {
|
|
153
|
+
console.error("[Extensions] setEnabled failed", e);
|
|
154
|
+
_detailError = e.message;
|
|
155
|
+
_emit("extensions:detail");
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
/** Remove an installed extension, then return to the list. */
|
|
160
|
+
async uninstall(id) {
|
|
161
|
+
if (!id) return;
|
|
162
|
+
try {
|
|
163
|
+
const res = await fetch("/api/store/extension", {
|
|
164
|
+
method: "DELETE",
|
|
165
|
+
headers: { "Content-Type": "application/json" },
|
|
166
|
+
body: JSON.stringify({ id }),
|
|
167
|
+
});
|
|
168
|
+
const data = await res.json();
|
|
169
|
+
if (!res.ok || !data.ok) throw new Error(data.error || "uninstall failed");
|
|
170
|
+
Extensions.closeDetail();
|
|
171
|
+
Extensions.load();
|
|
172
|
+
} catch (e) {
|
|
173
|
+
console.error("[Extensions] uninstall failed", e);
|
|
174
|
+
_detailError = e.message;
|
|
175
|
+
_emit("extensions:detail");
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
return Extensions;
|
|
181
|
+
})();
|
|
182
|
+
|
|
183
|
+
const Extensions = ExtensionsStore;
|
|
184
|
+
Clacky.Extensions = Extensions;
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// ── Extensions · view — rendering, DOM wiring for the extension marketplace ─
|
|
2
|
+
//
|
|
3
|
+
// The view owns everything DOM: the search bar, sort control, extension cards,
|
|
4
|
+
// loading/empty states. It reads data only through ExtensionsStore.state and
|
|
5
|
+
// reacts to store events via Extensions.on(...). It never fetches directly — it
|
|
6
|
+
// calls store actions.
|
|
7
|
+
//
|
|
8
|
+
// The panel is read-only: extensions are delivered inside license-gated brand
|
|
9
|
+
// packages, so there is no install button here, only a "how to get" hint.
|
|
10
|
+
//
|
|
11
|
+
// Depends on: ExtensionsStore (store.js), I18n/Router, global $ / escapeHtml.
|
|
12
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const ExtensionsView = (() => {
|
|
15
|
+
let _domWired = false;
|
|
16
|
+
let _searchTimer = null;
|
|
17
|
+
|
|
18
|
+
function _renderLoading() {
|
|
19
|
+
const container = $("extensions-list");
|
|
20
|
+
if (!container) return;
|
|
21
|
+
container.innerHTML = Array.from({ length: 4 }).map(() => `
|
|
22
|
+
<div class="extension-card">
|
|
23
|
+
<div class="extension-card-main">
|
|
24
|
+
<span class="skel" style="height:2rem;width:2rem;border-radius:8px;"></span>
|
|
25
|
+
<div class="extension-info">
|
|
26
|
+
<span class="skel skel-title"></span>
|
|
27
|
+
<span class="skel skel-subtitle"></span>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>`).join("");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function _renderEmpty() {
|
|
34
|
+
const container = $("extensions-list");
|
|
35
|
+
if (!container) return;
|
|
36
|
+
const key = ExtensionsStore.state.query ? "extensions.noResults" : "extensions.empty";
|
|
37
|
+
container.innerHTML = `<div class="extensions-empty">${escapeHtml(I18n.t(key))}</div>`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function _renderList() {
|
|
41
|
+
const container = $("extensions-list");
|
|
42
|
+
if (!container) return;
|
|
43
|
+
|
|
44
|
+
if (ExtensionsStore.state.loading) { _renderLoading(); return; }
|
|
45
|
+
|
|
46
|
+
const list = ExtensionsStore.state.extensions;
|
|
47
|
+
if (!list || list.length === 0) { _renderEmpty(); return; }
|
|
48
|
+
|
|
49
|
+
container.innerHTML = "";
|
|
50
|
+
list.forEach(ext => container.appendChild(_renderCard(ext)));
|
|
51
|
+
|
|
52
|
+
_applyWarning(ExtensionsStore.state.error);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function _applyWarning(warning) {
|
|
56
|
+
const banner = $("extensions-warning");
|
|
57
|
+
if (!banner) return;
|
|
58
|
+
if (warning) {
|
|
59
|
+
banner.textContent = warning;
|
|
60
|
+
banner.style.display = "";
|
|
61
|
+
} else {
|
|
62
|
+
banner.style.display = "none";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function _formatUnits(units) {
|
|
67
|
+
if (!units || typeof units !== "object") return "";
|
|
68
|
+
const parts = [];
|
|
69
|
+
Object.keys(units).forEach((type) => {
|
|
70
|
+
const n = parseInt(units[type], 10);
|
|
71
|
+
if (!n) return;
|
|
72
|
+
const key = "extensions.unit." + type + (n > 1 ? "s" : "");
|
|
73
|
+
const label = I18n.t(key);
|
|
74
|
+
const word = (label && label !== key) ? label : type;
|
|
75
|
+
parts.push(`${n} ${word}`);
|
|
76
|
+
});
|
|
77
|
+
return parts.join(" · ");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function _renderCard(ext) {
|
|
81
|
+
const currentLang = I18n.lang();
|
|
82
|
+
const name = (currentLang === "zh" && ext.name_zh) ? ext.name_zh : ext.name;
|
|
83
|
+
const description = (currentLang === "zh" && ext.description_zh)
|
|
84
|
+
? ext.description_zh
|
|
85
|
+
: ext.description || "";
|
|
86
|
+
const emoji = ext.emoji || "🧩";
|
|
87
|
+
|
|
88
|
+
const versionHtml = ext.version
|
|
89
|
+
? `<span class="extension-version">v${escapeHtml(String(ext.version))}</span>` : "";
|
|
90
|
+
const installedHtml = ext.installed
|
|
91
|
+
? `<span class="extension-installed">${escapeHtml(I18n.t("extensions.installed"))}</span>` : "";
|
|
92
|
+
const unitsText = _formatUnits(ext.units);
|
|
93
|
+
const unitsHtml = unitsText
|
|
94
|
+
? `<span class="extension-units">${escapeHtml(unitsText)}</span>` : "";
|
|
95
|
+
const homepageHtml = ext.homepage
|
|
96
|
+
? `<a class="extension-homepage" href="${escapeHtml(ext.homepage)}" target="_blank" rel="noopener noreferrer">${I18n.t("extensions.homepage")}</a>`
|
|
97
|
+
: "";
|
|
98
|
+
|
|
99
|
+
const card = document.createElement("div");
|
|
100
|
+
card.className = "extension-card extension-card-clickable";
|
|
101
|
+
card.dataset.extId = ext.id != null ? String(ext.id) : (ext.name || "");
|
|
102
|
+
card.innerHTML = `
|
|
103
|
+
<div class="extension-card-main">
|
|
104
|
+
<div class="extension-emoji">${escapeHtml(emoji)}</div>
|
|
105
|
+
<div class="extension-info">
|
|
106
|
+
<div class="extension-title">
|
|
107
|
+
<span class="extension-name">${escapeHtml(name)}</span>
|
|
108
|
+
${versionHtml}
|
|
109
|
+
${installedHtml}
|
|
110
|
+
${unitsHtml}
|
|
111
|
+
</div>
|
|
112
|
+
<div class="extension-desc">${escapeHtml(description)}</div>
|
|
113
|
+
<div class="extension-meta">
|
|
114
|
+
${homepageHtml}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>`;
|
|
118
|
+
return card;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function _renderDetail() {
|
|
122
|
+
const panel = $("extensions-detail");
|
|
123
|
+
const body = $("extensions-body");
|
|
124
|
+
if (!panel) return;
|
|
125
|
+
|
|
126
|
+
const st = ExtensionsStore.state;
|
|
127
|
+
const open = st.detail || st.detailLoading || st.detailError;
|
|
128
|
+
|
|
129
|
+
if (!open) {
|
|
130
|
+
panel.style.display = "none";
|
|
131
|
+
panel.innerHTML = "";
|
|
132
|
+
if (body) body.style.display = "";
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (body) body.style.display = "none";
|
|
137
|
+
panel.style.display = "";
|
|
138
|
+
|
|
139
|
+
if (st.detailLoading) {
|
|
140
|
+
panel.innerHTML = _detailShell(`
|
|
141
|
+
<div class="extension-detail-loading">
|
|
142
|
+
<span class="skel skel-title"></span>
|
|
143
|
+
<span class="skel skel-subtitle"></span>
|
|
144
|
+
</div>`);
|
|
145
|
+
_wireDetail();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (st.detailError) {
|
|
150
|
+
panel.innerHTML = _detailShell(`
|
|
151
|
+
<div class="extensions-empty">${escapeHtml(st.detailError)}</div>`);
|
|
152
|
+
_wireDetail();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
panel.innerHTML = _detailShell(_detailContent(st.detail));
|
|
157
|
+
_wireDetail();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function _detailShell(inner) {
|
|
161
|
+
return `
|
|
162
|
+
<div class="extension-detail-head">
|
|
163
|
+
<button type="button" class="extension-detail-back" id="extension-detail-back">
|
|
164
|
+
← ${escapeHtml(I18n.t("extensions.detail.back"))}
|
|
165
|
+
</button>
|
|
166
|
+
</div>
|
|
167
|
+
${inner}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function _wireDetail() {
|
|
171
|
+
const back = $("extension-detail-back");
|
|
172
|
+
if (back) back.addEventListener("click", () => _backToList());
|
|
173
|
+
|
|
174
|
+
const toggle = document.querySelector("[data-ext-toggle]");
|
|
175
|
+
if (toggle) {
|
|
176
|
+
toggle.addEventListener("click", () => {
|
|
177
|
+
const id = toggle.getAttribute("data-ext-toggle");
|
|
178
|
+
const currentlyDisabled = toggle.getAttribute("data-ext-enabled") === "1";
|
|
179
|
+
Extensions.setEnabled(id, currentlyDisabled);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const remove = document.querySelector("[data-ext-remove]");
|
|
184
|
+
if (remove) {
|
|
185
|
+
remove.addEventListener("click", async () => {
|
|
186
|
+
const id = remove.getAttribute("data-ext-remove");
|
|
187
|
+
const ok = await Modal.confirm(I18n.t("extensions.action.removeConfirm"));
|
|
188
|
+
if (ok) Extensions.uninstall(id);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function _backToList() {
|
|
194
|
+
const router = window.Clacky && window.Clacky.Router;
|
|
195
|
+
if (router) router.navigate("extensions");
|
|
196
|
+
else Extensions.closeDetail();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function _detailContent(ext) {
|
|
200
|
+
const currentLang = I18n.lang();
|
|
201
|
+
const name = (currentLang === "zh" && ext.name_zh) ? ext.name_zh : ext.name;
|
|
202
|
+
const description = (currentLang === "zh" && ext.description_zh)
|
|
203
|
+
? ext.description_zh
|
|
204
|
+
: ext.description || "";
|
|
205
|
+
const emoji = ext.emoji || "🧩";
|
|
206
|
+
|
|
207
|
+
const versionHtml = ext.version
|
|
208
|
+
? `<span class="extension-version">v${escapeHtml(String(ext.version))}</span>` : "";
|
|
209
|
+
const installedHtml = ext.installed
|
|
210
|
+
? `<span class="extension-installed">${escapeHtml(I18n.t("extensions.installed"))}</span>` : "";
|
|
211
|
+
const unitsText = _formatUnits(ext.units);
|
|
212
|
+
const unitsHtml = unitsText
|
|
213
|
+
? `<span class="extension-units">${escapeHtml(unitsText)}</span>` : "";
|
|
214
|
+
const homepageHtml = ext.homepage
|
|
215
|
+
? `<a class="extension-homepage" href="${escapeHtml(ext.homepage)}" target="_blank" rel="noopener noreferrer">${I18n.t("extensions.homepage")}</a>`
|
|
216
|
+
: "";
|
|
217
|
+
|
|
218
|
+
return `
|
|
219
|
+
<div class="extension-detail-hero">
|
|
220
|
+
<div class="extension-emoji extension-emoji-lg">${escapeHtml(emoji)}</div>
|
|
221
|
+
<div class="extension-detail-heading">
|
|
222
|
+
<div class="extension-title">
|
|
223
|
+
<span class="extension-name extension-name-lg">${escapeHtml(name)}</span>
|
|
224
|
+
${versionHtml}
|
|
225
|
+
${installedHtml}
|
|
226
|
+
${unitsHtml}
|
|
227
|
+
</div>
|
|
228
|
+
<div class="extension-desc">${escapeHtml(description)}</div>
|
|
229
|
+
<div class="extension-meta">
|
|
230
|
+
${homepageHtml}
|
|
231
|
+
</div>
|
|
232
|
+
${_renderActions(ext)}
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
${_renderContributes(ext.contributes)}
|
|
236
|
+
${_renderVersions(ext.versions)}`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Manage buttons for a locally installed extension: enable/disable toggle
|
|
240
|
+
// (always available when installed) plus remove (installed layer only).
|
|
241
|
+
function _renderActions(ext) {
|
|
242
|
+
if (!ext.installed) return "";
|
|
243
|
+
const slug = ext.name || ext.slug || (ext.id != null ? String(ext.id) : "");
|
|
244
|
+
const toggleKey = ext.disabled ? "extensions.action.enable" : "extensions.action.disable";
|
|
245
|
+
const toggleCls = ext.disabled ? "extension-action-enable" : "extension-action-disable";
|
|
246
|
+
const disabledBadge = ext.disabled
|
|
247
|
+
? `<span class="extension-disabled">${escapeHtml(I18n.t("extensions.disabled"))}</span>` : "";
|
|
248
|
+
const removeBtn = ext.removable
|
|
249
|
+
? `<button type="button" class="extension-action extension-action-remove" data-ext-remove="${escapeHtml(slug)}">${escapeHtml(I18n.t("extensions.action.remove"))}</button>`
|
|
250
|
+
: "";
|
|
251
|
+
return `
|
|
252
|
+
<div class="extension-detail-actions">
|
|
253
|
+
${disabledBadge}
|
|
254
|
+
<button type="button" class="extension-action ${toggleCls}" data-ext-toggle="${escapeHtml(slug)}" data-ext-enabled="${ext.disabled ? "1" : "0"}">${escapeHtml(I18n.t(toggleKey))}</button>
|
|
255
|
+
${removeBtn}
|
|
256
|
+
</div>`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function _sectionHeading(type) {
|
|
260
|
+
const key = "extensions.section." + type;
|
|
261
|
+
const label = I18n.t(key);
|
|
262
|
+
return (label && label !== key) ? label : type;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function _renderContributes(contributes) {
|
|
266
|
+
if (!contributes || typeof contributes !== "object") return "";
|
|
267
|
+
const currentLang = I18n.lang();
|
|
268
|
+
const sections = [];
|
|
269
|
+
|
|
270
|
+
Object.keys(contributes).forEach((type) => {
|
|
271
|
+
const items = contributes[type];
|
|
272
|
+
if (!Array.isArray(items) || items.length === 0) return;
|
|
273
|
+
const singular = type.replace(/s$/, "");
|
|
274
|
+
const heading = _sectionHeading(type);
|
|
275
|
+
const rows = items.map((it) => {
|
|
276
|
+
const title = (currentLang === "zh" && it.title_zh) ? it.title_zh
|
|
277
|
+
: (it.title || it.name || it.id || singular);
|
|
278
|
+
const desc = (currentLang === "zh" && it.description_zh) ? it.description_zh
|
|
279
|
+
: (it.description || "");
|
|
280
|
+
return `
|
|
281
|
+
<li class="extension-contrib-item">
|
|
282
|
+
<span class="extension-contrib-title">${escapeHtml(String(title))}</span>
|
|
283
|
+
${desc ? `<span class="extension-contrib-desc">${escapeHtml(String(desc))}</span>` : ""}
|
|
284
|
+
</li>`;
|
|
285
|
+
}).join("");
|
|
286
|
+
sections.push(`
|
|
287
|
+
<div class="extension-detail-section">
|
|
288
|
+
<h4 class="extension-detail-section-title">${escapeHtml(heading)}</h4>
|
|
289
|
+
<ul class="extension-contrib-list">${rows}</ul>
|
|
290
|
+
</div>`);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
if (sections.length === 0) return "";
|
|
294
|
+
return `
|
|
295
|
+
<div class="extension-detail-block">
|
|
296
|
+
<h3 class="extension-detail-block-title">${escapeHtml(I18n.t("extensions.detail.contributes"))}</h3>
|
|
297
|
+
${sections.join("")}
|
|
298
|
+
</div>`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function _renderVersions(versions) {
|
|
302
|
+
if (!Array.isArray(versions) || versions.length === 0) return "";
|
|
303
|
+
const rows = versions.map((v) => {
|
|
304
|
+
const date = v.published_at ? String(v.published_at).slice(0, 10) : "";
|
|
305
|
+
return `
|
|
306
|
+
<li class="extension-version-item">
|
|
307
|
+
<div class="extension-version-row">
|
|
308
|
+
<span class="extension-version">v${escapeHtml(String(v.version || ""))}</span>
|
|
309
|
+
${date ? `<span class="extension-version-date">${escapeHtml(date)}</span>` : ""}
|
|
310
|
+
</div>
|
|
311
|
+
${v.release_notes ? `<div class="extension-version-notes">${escapeHtml(String(v.release_notes))}</div>` : ""}
|
|
312
|
+
</li>`;
|
|
313
|
+
}).join("");
|
|
314
|
+
return `
|
|
315
|
+
<div class="extension-detail-block">
|
|
316
|
+
<h3 class="extension-detail-block-title">${escapeHtml(I18n.t("extensions.detail.versions"))}</h3>
|
|
317
|
+
<ul class="extension-version-list">${rows}</ul>
|
|
318
|
+
</div>`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function _wireDom() {
|
|
322
|
+
if (_domWired) return;
|
|
323
|
+
|
|
324
|
+
const input = $("extensions-search-input");
|
|
325
|
+
if (input) {
|
|
326
|
+
input.placeholder = I18n.t("extensions.searchPlaceholder");
|
|
327
|
+
input.addEventListener("input", () => {
|
|
328
|
+
clearTimeout(_searchTimer);
|
|
329
|
+
_searchTimer = setTimeout(() => Extensions.setQuery(input.value), 300);
|
|
330
|
+
});
|
|
331
|
+
input.addEventListener("keydown", (e) => {
|
|
332
|
+
if (e.key === "Enter") { e.preventDefault(); clearTimeout(_searchTimer); Extensions.setQuery(input.value); }
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const sortSelect = $("extensions-sort");
|
|
337
|
+
if (sortSelect) {
|
|
338
|
+
sortSelect.value = ExtensionsStore.state.sort;
|
|
339
|
+
sortSelect.addEventListener("change", () => Extensions.setSort(sortSelect.value));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const list = $("extensions-list");
|
|
343
|
+
if (list) {
|
|
344
|
+
list.addEventListener("click", (e) => {
|
|
345
|
+
if (e.target.closest("a")) return;
|
|
346
|
+
const card = e.target.closest(".extension-card-clickable");
|
|
347
|
+
if (card && card.dataset.extId) {
|
|
348
|
+
const router = window.Clacky && window.Clacky.Router;
|
|
349
|
+
if (router) router.navigate("extensions", { detailId: card.dataset.extId });
|
|
350
|
+
else Extensions.loadDetail(card.dataset.extId);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
document.addEventListener("langchange", () => {
|
|
356
|
+
if (input) input.placeholder = I18n.t("extensions.searchPlaceholder");
|
|
357
|
+
_renderList();
|
|
358
|
+
_renderDetail();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
_domWired = true;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function _subscribe() {
|
|
365
|
+
Extensions.on("extensions:loading", _renderLoading);
|
|
366
|
+
Extensions.on("extensions:changed", _renderList);
|
|
367
|
+
Extensions.on("extensions:error", _renderList);
|
|
368
|
+
Extensions.on("extensions:detail", _renderDetail);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const viewApi = {
|
|
372
|
+
onPanelShow(opts) {
|
|
373
|
+
_wireDom();
|
|
374
|
+
const detailId = opts && opts.detailId;
|
|
375
|
+
if (detailId) {
|
|
376
|
+
Extensions.load();
|
|
377
|
+
Extensions.loadDetail(detailId);
|
|
378
|
+
} else {
|
|
379
|
+
Extensions.closeDetail();
|
|
380
|
+
Extensions.load();
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
return { init: _subscribe, api: viewApi };
|
|
386
|
+
})();
|
|
387
|
+
|
|
388
|
+
Object.assign(Extensions, ExtensionsView.api);
|
|
389
|
+
ExtensionsView.init();
|