@harness-mix/cli 0.1.9 → 0.1.10
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/CHANGELOG.md +7 -0
- package/README.md +25 -30
- package/output/native-build/desktop-controller.mjs +1 -1
- package/output/native-build/renderer-extension.js +703 -41
- package/package.json +9 -8
- package/scripts/native-config-test.cjs +32 -2
- package/scripts/native-ui-smoke.cjs +39 -4
- package/scripts/pets-test.cjs +146 -0
- package/src/main/native/launcher.js +49 -2
- package/src/main/native/pets.js +176 -15
- package/src/main/native/protocol.js +9 -8
- package/src/native-ui/desktop-control/dist/renderer-draft-prewarm-runtime.js +4 -4
- package/src/native-ui/desktop-control/dist/renderer-draft-prewarm-runtime.js.map +1 -1
- package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/desktop-control/src/renderer-draft-prewarm-runtime.ts +4 -4
- package/src/native-ui/desktop-control/test/renderer-draft-prewarm-policy.test.ts +7 -0
- package/src/native-ui/renderer-extension/dist/types/agent-selection-state.d.ts +1 -0
- package/src/native-ui/renderer-extension/dist/types/agent-selection-state.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +2 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts +2 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-settings-lifecycle.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/icons.d.ts +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/icons.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/localization.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts +5 -3
- package/src/native-ui/renderer-extension/dist/types/settings/pages.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/pet-market.d.ts +5 -0
- package/src/native-ui/renderer-extension/dist/types/settings/pet-market.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/pets-client.d.ts +57 -0
- package/src/native-ui/renderer-extension/dist/types/settings/pets-client.d.ts.map +1 -0
- package/src/native-ui/renderer-extension/dist/types/settings/shell.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/settings/skin-runtime.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/renderer-extension/src/agent-selection-state.ts +45 -14
- package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +69 -27
- package/src/native-ui/renderer-extension/src/renderer-model-client.ts +6 -0
- package/src/native-ui/renderer-extension/src/renderer-settings-lifecycle.ts +3 -0
- package/src/native-ui/renderer-extension/src/settings/icons.ts +15 -12
- package/src/native-ui/renderer-extension/src/settings/localization.ts +2 -0
- package/src/native-ui/renderer-extension/src/settings/pages.ts +74 -66
- package/src/native-ui/renderer-extension/src/settings/pet-market.css +310 -0
- package/src/native-ui/renderer-extension/src/settings/pet-market.ts +447 -0
- package/src/native-ui/renderer-extension/src/settings/pets-client.ts +74 -0
- package/src/native-ui/renderer-extension/src/settings/shell.css +27 -8
- package/src/native-ui/renderer-extension/src/settings/shell.ts +4 -3
- package/src/native-ui/renderer-extension/src/settings/skin-runtime.ts +234 -3
- package/src/native-ui/renderer-extension/test/agent-selection-state.test.ts +58 -0
- package/src/native-ui/renderer-extension/test/renderer-binding-probe.test.ts +12 -0
- package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +1 -0
- package/src/native-ui/renderer-extension/test/settings/localization.test.ts +1 -0
- package/src/native-ui/renderer-extension/test/settings/shell.test.ts +3 -0
- package/src/native-ui/renderer-extension/test/settings/skin-runtime.test.ts +28 -2
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import type { RendererSettingsMessages } from "./localization.js";
|
|
2
|
+
import type {
|
|
3
|
+
RendererSettingsPageDefinition,
|
|
4
|
+
RendererSettingsPageMountContext,
|
|
5
|
+
} from "./core.js";
|
|
6
|
+
import { createRendererSettingsIcon } from "./icons.js";
|
|
7
|
+
import type { RendererPetsClient, RendererPetItem } from "./pets-client.js";
|
|
8
|
+
|
|
9
|
+
const COPY = {
|
|
10
|
+
en: {
|
|
11
|
+
title: "Pets",
|
|
12
|
+
intro:
|
|
13
|
+
"Add companion pets to Codex Desktop. Installed companions sync directly with Codex native pets directory and respond to agent state.",
|
|
14
|
+
safe: "Native local integration",
|
|
15
|
+
safeDetail:
|
|
16
|
+
"Pets install into ~/.codex/pets/, containing only spritesheets and metadata without running external scripts. Instantly selectable in Codex settings.",
|
|
17
|
+
all: "All",
|
|
18
|
+
official: "Official Preload",
|
|
19
|
+
community: "Community",
|
|
20
|
+
installed: "Installed",
|
|
21
|
+
searchPlaceholder: "Search pets by name or description...",
|
|
22
|
+
install: "Install Pet",
|
|
23
|
+
installing: "Installing...",
|
|
24
|
+
installedBadge: "Installed",
|
|
25
|
+
uninstall: "Uninstall",
|
|
26
|
+
uninstalling: "Removing...",
|
|
27
|
+
tip: "Select in Appearance > Pets or type /pet to wake",
|
|
28
|
+
empty: "No pets match the current filter.",
|
|
29
|
+
officialBadge: "Official",
|
|
30
|
+
communityBadge: "Community",
|
|
31
|
+
},
|
|
32
|
+
"zh-CN": {
|
|
33
|
+
title: "桌宠市场",
|
|
34
|
+
intro:
|
|
35
|
+
"为 Codex 添加桌面萌宠伴侣。安装后自动同步至 Codex Desktop 原生桌宠目录,随 AI 状态实时交互动画。",
|
|
36
|
+
safe: "本地原生集成",
|
|
37
|
+
safeDetail:
|
|
38
|
+
"桌宠安装于 ~/.codex/pets/,仅包含动画精灵图与元数据,不执行外部代码。在 Codex 官方外观设置中立即可选。",
|
|
39
|
+
all: "全部",
|
|
40
|
+
official: "官方预载",
|
|
41
|
+
community: "社区精选",
|
|
42
|
+
installed: "已安装",
|
|
43
|
+
searchPlaceholder: "搜索桌宠名称或描述...",
|
|
44
|
+
install: "安装桌宠",
|
|
45
|
+
installing: "安装中...",
|
|
46
|
+
installedBadge: "已安装",
|
|
47
|
+
uninstall: "卸载",
|
|
48
|
+
uninstalling: "卸载中...",
|
|
49
|
+
tip: "安装后在官方设置「外观 > 桌宠」或输入 /pet 指令唤醒伴侣",
|
|
50
|
+
empty: "没有找到符合条件的桌宠。",
|
|
51
|
+
officialBadge: "官方预载",
|
|
52
|
+
communityBadge: "社区精选",
|
|
53
|
+
},
|
|
54
|
+
} as const;
|
|
55
|
+
|
|
56
|
+
type PetTab = "all" | "official" | "community" | "installed";
|
|
57
|
+
|
|
58
|
+
const SPRITE_COLUMNS = 8;
|
|
59
|
+
const SPRITE_FRAMES = 6;
|
|
60
|
+
const FRAME_DURATION_MS = 220;
|
|
61
|
+
|
|
62
|
+
function setupSpriteAnimation(
|
|
63
|
+
element: HTMLElement,
|
|
64
|
+
imageUrl: string,
|
|
65
|
+
spriteVersion = 2,
|
|
66
|
+
): () => void {
|
|
67
|
+
const rows = spriteVersion === 1 ? 9 : 11;
|
|
68
|
+
element.style.backgroundImage = `url("${imageUrl}")`;
|
|
69
|
+
element.style.backgroundSize = `${SPRITE_COLUMNS * 100}% ${rows * 100}%`;
|
|
70
|
+
element.style.backgroundPosition = "0% 0%";
|
|
71
|
+
|
|
72
|
+
let currentFrame = 0;
|
|
73
|
+
let timer: number | null = null;
|
|
74
|
+
let isHovered = false;
|
|
75
|
+
|
|
76
|
+
const tick = () => {
|
|
77
|
+
currentFrame = (currentFrame + 1) % SPRITE_FRAMES;
|
|
78
|
+
const posX = (currentFrame / (SPRITE_COLUMNS - 1)) * 100;
|
|
79
|
+
element.style.backgroundPosition = `${posX}% 0%`;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const startLoop = () => {
|
|
83
|
+
if (timer !== null) return;
|
|
84
|
+
timer = window.setInterval(tick, FRAME_DURATION_MS);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const stopLoop = () => {
|
|
88
|
+
if (timer !== null) {
|
|
89
|
+
clearInterval(timer);
|
|
90
|
+
timer = null;
|
|
91
|
+
}
|
|
92
|
+
if (!isHovered) {
|
|
93
|
+
currentFrame = 0;
|
|
94
|
+
element.style.backgroundPosition = "0% 0%";
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const onMouseEnter = () => {
|
|
99
|
+
isHovered = true;
|
|
100
|
+
startLoop();
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const onMouseLeave = () => {
|
|
104
|
+
isHovered = false;
|
|
105
|
+
stopLoop();
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const parentCard = element.closest(".pet-card");
|
|
109
|
+
if (parentCard) {
|
|
110
|
+
parentCard.addEventListener("mouseenter", onMouseEnter);
|
|
111
|
+
parentCard.addEventListener("mouseleave", onMouseLeave);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Periodic idle blink/movement every few seconds
|
|
115
|
+
const idleInterval = window.setInterval(() => {
|
|
116
|
+
if (!isHovered && timer === null) {
|
|
117
|
+
startLoop();
|
|
118
|
+
setTimeout(stopLoop, FRAME_DURATION_MS * SPRITE_FRAMES);
|
|
119
|
+
}
|
|
120
|
+
}, 4000 + Math.random() * 3000);
|
|
121
|
+
|
|
122
|
+
return () => {
|
|
123
|
+
stopLoop();
|
|
124
|
+
clearInterval(idleInterval);
|
|
125
|
+
if (parentCard) {
|
|
126
|
+
parentCard.removeEventListener("mouseenter", onMouseEnter);
|
|
127
|
+
parentCard.removeEventListener("mouseleave", onMouseLeave);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function createPetSettingsPage(
|
|
133
|
+
messages: RendererSettingsMessages,
|
|
134
|
+
getClient: () => RendererPetsClient | null,
|
|
135
|
+
): RendererSettingsPageDefinition {
|
|
136
|
+
const copy = COPY[messages.locale];
|
|
137
|
+
|
|
138
|
+
return Object.freeze({
|
|
139
|
+
id: "pets",
|
|
140
|
+
label: messages.pageLabels.pets,
|
|
141
|
+
icon: "pets",
|
|
142
|
+
mount(context: RendererSettingsPageMountContext) {
|
|
143
|
+
const document = context.content.ownerDocument;
|
|
144
|
+
const disposers: Array<() => void> = [];
|
|
145
|
+
|
|
146
|
+
const heading = document.createElement("div");
|
|
147
|
+
heading.className = "settings-section-label";
|
|
148
|
+
heading.textContent = copy.title;
|
|
149
|
+
|
|
150
|
+
const intro = document.createElement("p");
|
|
151
|
+
intro.className = "pet-market__intro";
|
|
152
|
+
intro.textContent = copy.intro;
|
|
153
|
+
|
|
154
|
+
const safety = document.createElement("aside");
|
|
155
|
+
safety.className = "pet-safety-note";
|
|
156
|
+
safety.append(createRendererSettingsIcon("shield", 18));
|
|
157
|
+
const safetyBody = document.createElement("span");
|
|
158
|
+
const safetyTitle = document.createElement("strong");
|
|
159
|
+
safetyTitle.textContent = copy.safe;
|
|
160
|
+
const safetyDetail = document.createElement("span");
|
|
161
|
+
safetyDetail.textContent = copy.safeDetail;
|
|
162
|
+
safetyBody.append(safetyTitle, safetyDetail);
|
|
163
|
+
safety.append(safetyBody);
|
|
164
|
+
|
|
165
|
+
// Toolbar: tabs + search
|
|
166
|
+
const toolbar = document.createElement("div");
|
|
167
|
+
toolbar.className = "pet-market__toolbar";
|
|
168
|
+
|
|
169
|
+
const tabsWrap = document.createElement("div");
|
|
170
|
+
tabsWrap.className = "pet-market__tabs";
|
|
171
|
+
|
|
172
|
+
let activeTab: PetTab = "all";
|
|
173
|
+
let searchQuery = "";
|
|
174
|
+
const tabButtons = new Map<PetTab, HTMLButtonElement>();
|
|
175
|
+
|
|
176
|
+
const tabs: Array<{ id: PetTab; label: string }> = [
|
|
177
|
+
{ id: "all", label: copy.all },
|
|
178
|
+
{ id: "official", label: copy.official },
|
|
179
|
+
{ id: "community", label: copy.community },
|
|
180
|
+
{ id: "installed", label: copy.installed },
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
const searchWrap = document.createElement("div");
|
|
184
|
+
searchWrap.className = "pet-market__search-wrap";
|
|
185
|
+
searchWrap.append(createRendererSettingsIcon("search", 15));
|
|
186
|
+
|
|
187
|
+
const searchInput = document.createElement("input");
|
|
188
|
+
searchInput.type = "search";
|
|
189
|
+
searchInput.className = "pet-market__search";
|
|
190
|
+
searchInput.placeholder = copy.searchPlaceholder;
|
|
191
|
+
searchWrap.append(searchInput);
|
|
192
|
+
|
|
193
|
+
toolbar.append(tabsWrap, searchWrap);
|
|
194
|
+
|
|
195
|
+
const grid = document.createElement("div");
|
|
196
|
+
grid.className = "pet-market-grid";
|
|
197
|
+
|
|
198
|
+
context.content.append(heading, intro, safety, toolbar, grid);
|
|
199
|
+
|
|
200
|
+
let allPets: RendererPetItem[] = [];
|
|
201
|
+
const imageCache = new Map<string, string>(); // id -> dataUrl / url
|
|
202
|
+
|
|
203
|
+
const renderGrid = () => {
|
|
204
|
+
// Clear previous animations
|
|
205
|
+
for (const d of disposers) d();
|
|
206
|
+
disposers.length = 0;
|
|
207
|
+
grid.replaceChildren();
|
|
208
|
+
|
|
209
|
+
const query = searchQuery.trim().toLowerCase();
|
|
210
|
+
const filtered = allPets.filter((pet) => {
|
|
211
|
+
if (activeTab === "official" && pet.source !== "official") return false;
|
|
212
|
+
if (activeTab === "community" && pet.source !== "community") return false;
|
|
213
|
+
if (activeTab === "installed" && !pet.installed) return false;
|
|
214
|
+
if (query) {
|
|
215
|
+
const matchesName = pet.displayName.toLowerCase().includes(query);
|
|
216
|
+
const matchesDesc = (pet.description || "").toLowerCase().includes(query);
|
|
217
|
+
const matchesId = pet.id.toLowerCase().includes(query);
|
|
218
|
+
if (!matchesName && !matchesDesc && !matchesId) return false;
|
|
219
|
+
}
|
|
220
|
+
return true;
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (filtered.length === 0) {
|
|
224
|
+
const empty = document.createElement("div");
|
|
225
|
+
empty.className = "pet-market__empty";
|
|
226
|
+
empty.textContent = copy.empty;
|
|
227
|
+
grid.append(empty);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for (const pet of filtered) {
|
|
232
|
+
const card = document.createElement("article");
|
|
233
|
+
card.className = "pet-card";
|
|
234
|
+
card.dataset.petId = pet.id;
|
|
235
|
+
card.dataset.installed = String(pet.installed);
|
|
236
|
+
|
|
237
|
+
// Stage
|
|
238
|
+
const stage = document.createElement("div");
|
|
239
|
+
stage.className = "pet-card__stage";
|
|
240
|
+
|
|
241
|
+
const sprite = document.createElement("div");
|
|
242
|
+
sprite.className = "pet-card__sprite";
|
|
243
|
+
stage.append(sprite);
|
|
244
|
+
|
|
245
|
+
// Load visual asset
|
|
246
|
+
const client = getClient();
|
|
247
|
+
const cachedImg = imageCache.get(pet.id);
|
|
248
|
+
if (cachedImg) {
|
|
249
|
+
const disposeAnim = setupSpriteAnimation(sprite, cachedImg, pet.spriteVersionNumber);
|
|
250
|
+
disposers.push(disposeAnim);
|
|
251
|
+
} else if (pet.spritesheetUrl) {
|
|
252
|
+
imageCache.set(pet.id, pet.spritesheetUrl);
|
|
253
|
+
const disposeAnim = setupSpriteAnimation(
|
|
254
|
+
sprite,
|
|
255
|
+
pet.spritesheetUrl,
|
|
256
|
+
pet.spriteVersionNumber,
|
|
257
|
+
);
|
|
258
|
+
disposers.push(disposeAnim);
|
|
259
|
+
} else if (client) {
|
|
260
|
+
// Load base64 preview on demand
|
|
261
|
+
void client.preview(pet.id).then(
|
|
262
|
+
(res) => {
|
|
263
|
+
if (res && res.dataBase64) {
|
|
264
|
+
const dataUrl = `data:${res.mime};base64,${res.dataBase64}`;
|
|
265
|
+
imageCache.set(pet.id, dataUrl);
|
|
266
|
+
const disposeAnim = setupSpriteAnimation(
|
|
267
|
+
sprite,
|
|
268
|
+
dataUrl,
|
|
269
|
+
pet.spriteVersionNumber,
|
|
270
|
+
);
|
|
271
|
+
disposers.push(disposeAnim);
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
() => {
|
|
275
|
+
if (pet.previewUrl) {
|
|
276
|
+
sprite.style.backgroundImage = `url("${pet.previewUrl}")`;
|
|
277
|
+
sprite.style.backgroundSize = "contain";
|
|
278
|
+
sprite.style.backgroundPosition = "center";
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
);
|
|
282
|
+
} else if (pet.previewUrl) {
|
|
283
|
+
sprite.style.backgroundImage = `url("${pet.previewUrl}")`;
|
|
284
|
+
sprite.style.backgroundSize = "contain";
|
|
285
|
+
sprite.style.backgroundPosition = "center";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Body
|
|
289
|
+
const body = document.createElement("div");
|
|
290
|
+
body.className = "pet-card__body";
|
|
291
|
+
|
|
292
|
+
const cardHeader = document.createElement("div");
|
|
293
|
+
cardHeader.className = "pet-card__header";
|
|
294
|
+
|
|
295
|
+
const title = document.createElement("strong");
|
|
296
|
+
title.className = "pet-card__title";
|
|
297
|
+
title.textContent = pet.displayName;
|
|
298
|
+
|
|
299
|
+
const badges = document.createElement("div");
|
|
300
|
+
badges.className = "pet-card__badges";
|
|
301
|
+
|
|
302
|
+
if (pet.installed) {
|
|
303
|
+
const b = document.createElement("span");
|
|
304
|
+
b.className = "pet-card__badge pet-card__badge--installed";
|
|
305
|
+
b.append(createRendererSettingsIcon("check", 12), copy.installedBadge);
|
|
306
|
+
badges.append(b);
|
|
307
|
+
} else if (pet.source === "official") {
|
|
308
|
+
const b = document.createElement("span");
|
|
309
|
+
b.className = "pet-card__badge pet-card__badge--official";
|
|
310
|
+
b.textContent = copy.officialBadge;
|
|
311
|
+
badges.append(b);
|
|
312
|
+
} else if (pet.source === "community") {
|
|
313
|
+
const b = document.createElement("span");
|
|
314
|
+
b.className = "pet-card__badge pet-card__badge--community";
|
|
315
|
+
b.textContent = copy.communityBadge;
|
|
316
|
+
badges.append(b);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
cardHeader.append(title, badges);
|
|
320
|
+
|
|
321
|
+
const desc = document.createElement("p");
|
|
322
|
+
desc.className = "pet-card__description";
|
|
323
|
+
desc.textContent = pet.description || "";
|
|
324
|
+
|
|
325
|
+
// Footer & Actions
|
|
326
|
+
const footer = document.createElement("div");
|
|
327
|
+
footer.className = "pet-card__footer";
|
|
328
|
+
|
|
329
|
+
const hint = document.createElement("span");
|
|
330
|
+
hint.className = "pet-card__dir-hint";
|
|
331
|
+
hint.textContent = copy.tip;
|
|
332
|
+
|
|
333
|
+
const actions = document.createElement("div");
|
|
334
|
+
actions.className = "pet-card__actions";
|
|
335
|
+
|
|
336
|
+
if (pet.installed) {
|
|
337
|
+
const uninstallBtn = document.createElement("button");
|
|
338
|
+
uninstallBtn.type = "button";
|
|
339
|
+
uninstallBtn.className = "pet-btn pet-btn--danger";
|
|
340
|
+
uninstallBtn.textContent = copy.uninstall;
|
|
341
|
+
uninstallBtn.addEventListener("click", () => {
|
|
342
|
+
if (!client) return;
|
|
343
|
+
uninstallBtn.disabled = true;
|
|
344
|
+
uninstallBtn.textContent = copy.uninstalling;
|
|
345
|
+
void client.uninstall(pet.id).then(
|
|
346
|
+
() => {
|
|
347
|
+
allPets = allPets.map((p) =>
|
|
348
|
+
p.id === pet.id ? { ...p, installed: false } : p,
|
|
349
|
+
);
|
|
350
|
+
renderGrid();
|
|
351
|
+
},
|
|
352
|
+
(err) => {
|
|
353
|
+
uninstallBtn.disabled = false;
|
|
354
|
+
uninstallBtn.textContent = copy.uninstall;
|
|
355
|
+
alert(err instanceof Error ? err.message : String(err));
|
|
356
|
+
},
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
actions.append(uninstallBtn);
|
|
360
|
+
} else {
|
|
361
|
+
const installBtn = document.createElement("button");
|
|
362
|
+
installBtn.type = "button";
|
|
363
|
+
installBtn.className = "pet-btn pet-btn--primary";
|
|
364
|
+
installBtn.textContent = copy.install;
|
|
365
|
+
installBtn.addEventListener("click", () => {
|
|
366
|
+
if (!client) return;
|
|
367
|
+
installBtn.disabled = true;
|
|
368
|
+
installBtn.textContent = copy.installing;
|
|
369
|
+
void client
|
|
370
|
+
.install({
|
|
371
|
+
id: pet.id,
|
|
372
|
+
displayName: pet.displayName,
|
|
373
|
+
description: pet.description,
|
|
374
|
+
spritesheetUrl: pet.spritesheetUrl,
|
|
375
|
+
spriteVersionNumber: pet.spriteVersionNumber,
|
|
376
|
+
})
|
|
377
|
+
.then(
|
|
378
|
+
() => {
|
|
379
|
+
allPets = allPets.map((p) =>
|
|
380
|
+
p.id === pet.id ? { ...p, installed: true } : p,
|
|
381
|
+
);
|
|
382
|
+
renderGrid();
|
|
383
|
+
},
|
|
384
|
+
(err) => {
|
|
385
|
+
installBtn.disabled = false;
|
|
386
|
+
installBtn.textContent = copy.install;
|
|
387
|
+
alert(err instanceof Error ? err.message : String(err));
|
|
388
|
+
},
|
|
389
|
+
);
|
|
390
|
+
});
|
|
391
|
+
actions.append(installBtn);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
footer.append(hint, actions);
|
|
395
|
+
body.append(cardHeader, desc, footer);
|
|
396
|
+
card.append(stage, body);
|
|
397
|
+
grid.append(card);
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
for (const t of tabs) {
|
|
402
|
+
const btn = document.createElement("button");
|
|
403
|
+
btn.type = "button";
|
|
404
|
+
btn.className = "pet-market__tab";
|
|
405
|
+
btn.textContent = t.label;
|
|
406
|
+
btn.dataset.active = String(t.id === activeTab);
|
|
407
|
+
btn.addEventListener("click", () => {
|
|
408
|
+
activeTab = t.id;
|
|
409
|
+
for (const [tabId, b] of tabButtons) {
|
|
410
|
+
b.dataset.active = String(tabId === activeTab);
|
|
411
|
+
}
|
|
412
|
+
renderGrid();
|
|
413
|
+
});
|
|
414
|
+
tabButtons.set(t.id, btn);
|
|
415
|
+
tabsWrap.append(btn);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
searchInput.addEventListener("input", () => {
|
|
419
|
+
searchQuery = searchInput.value;
|
|
420
|
+
renderGrid();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// Fetch catalog
|
|
424
|
+
const client = getClient();
|
|
425
|
+
if (client) {
|
|
426
|
+
void context.runLatest(
|
|
427
|
+
() => client.catalog(),
|
|
428
|
+
{
|
|
429
|
+
success(result) {
|
|
430
|
+
allPets = [...result.data];
|
|
431
|
+
renderGrid();
|
|
432
|
+
},
|
|
433
|
+
failure(error) {
|
|
434
|
+
grid.textContent =
|
|
435
|
+
error instanceof Error ? error.message : String(error);
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return () => {
|
|
442
|
+
for (const d of disposers) d();
|
|
443
|
+
disposers.length = 0;
|
|
444
|
+
};
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export interface RendererPetItem {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly displayName: string;
|
|
4
|
+
readonly description?: string | undefined;
|
|
5
|
+
readonly source: "official" | "community" | "installed";
|
|
6
|
+
readonly installed: boolean;
|
|
7
|
+
readonly spriteVersionNumber?: number | undefined;
|
|
8
|
+
readonly spritesheetUrl?: string | undefined;
|
|
9
|
+
readonly previewUrl?: string | undefined;
|
|
10
|
+
readonly posterUrl?: string | undefined;
|
|
11
|
+
readonly downloadUrl?: string | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RendererPetCatalogResult {
|
|
15
|
+
readonly data: readonly RendererPetItem[];
|
|
16
|
+
readonly officialAvailable: boolean;
|
|
17
|
+
readonly petsDir: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface RendererPetCommunityResult {
|
|
21
|
+
readonly data: readonly RendererPetItem[];
|
|
22
|
+
readonly total: number;
|
|
23
|
+
readonly source: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RendererPetPreviewResult {
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly mime: string;
|
|
29
|
+
readonly dataBase64: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RendererPetInstallParams {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly displayName?: string | undefined;
|
|
35
|
+
readonly description?: string | undefined;
|
|
36
|
+
readonly spritesheetUrl?: string | undefined;
|
|
37
|
+
readonly spriteVersionNumber?: number | undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RendererPetInstallResult {
|
|
41
|
+
readonly id: string;
|
|
42
|
+
readonly path: string;
|
|
43
|
+
readonly installed: boolean;
|
|
44
|
+
readonly alreadyInstalled?: boolean | undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RendererPetsClient {
|
|
48
|
+
catalog(): Promise<RendererPetCatalogResult>;
|
|
49
|
+
community(query?: {
|
|
50
|
+
page?: number;
|
|
51
|
+
pageSize?: number;
|
|
52
|
+
sort?: string;
|
|
53
|
+
search?: string;
|
|
54
|
+
}): Promise<RendererPetCommunityResult>;
|
|
55
|
+
preview(id: string): Promise<RendererPetPreviewResult>;
|
|
56
|
+
install(params: RendererPetInstallParams): Promise<RendererPetInstallResult>;
|
|
57
|
+
uninstall(id: string): Promise<{ id: string; removed: boolean }>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createRendererPetsClient(
|
|
61
|
+
send: (method: string, params: unknown) => Promise<unknown>,
|
|
62
|
+
): RendererPetsClient {
|
|
63
|
+
return {
|
|
64
|
+
catalog: async () => (await send("codexhost/pets/catalog", {})) as RendererPetCatalogResult,
|
|
65
|
+
community: async (query = {}) =>
|
|
66
|
+
(await send("codexhost/pets/community", query)) as RendererPetCommunityResult,
|
|
67
|
+
preview: async (id: string) =>
|
|
68
|
+
(await send("codexhost/pets/preview", { id })) as RendererPetPreviewResult,
|
|
69
|
+
install: async (params: RendererPetInstallParams) =>
|
|
70
|
+
(await send("codexhost/pets/install", params)) as RendererPetInstallResult,
|
|
71
|
+
uninstall: async (id: string) =>
|
|
72
|
+
(await send("codexhost/pets/uninstall", { id })) as { id: string; removed: boolean },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -68,11 +68,13 @@ button {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
.codexhost-settings-dialog {
|
|
71
|
-
width: min(1120px, calc(100vw -
|
|
72
|
-
height: min(780px, calc(100vh -
|
|
71
|
+
width: min(1120px, calc(100vw - 48px));
|
|
72
|
+
height: min(780px, calc(100vh - 88px));
|
|
73
73
|
max-width: none;
|
|
74
|
-
max-height:
|
|
75
|
-
margin:
|
|
74
|
+
max-height: calc(100vh - 88px);
|
|
75
|
+
margin-top: max(56px, calc((100vh - 780px) / 2));
|
|
76
|
+
margin-inline: auto;
|
|
77
|
+
margin-bottom: auto;
|
|
76
78
|
padding: 0;
|
|
77
79
|
overflow: hidden;
|
|
78
80
|
color: var(--settings-text);
|
|
@@ -122,9 +124,9 @@ button {
|
|
|
122
124
|
display: flex;
|
|
123
125
|
align-items: center;
|
|
124
126
|
min-width: 0;
|
|
125
|
-
height:
|
|
127
|
+
height: 50px;
|
|
126
128
|
flex: none;
|
|
127
|
-
padding: 0
|
|
129
|
+
padding: 0 16px;
|
|
128
130
|
border-bottom: 1px solid var(--settings-border);
|
|
129
131
|
}
|
|
130
132
|
|
|
@@ -271,8 +273,8 @@ button {
|
|
|
271
273
|
display: inline-flex;
|
|
272
274
|
align-items: center;
|
|
273
275
|
justify-content: center;
|
|
274
|
-
width:
|
|
275
|
-
height:
|
|
276
|
+
width: 32px;
|
|
277
|
+
height: 32px;
|
|
276
278
|
flex: none;
|
|
277
279
|
padding: 0;
|
|
278
280
|
color: var(--settings-muted);
|
|
@@ -280,6 +282,7 @@ button {
|
|
|
280
282
|
border: 0;
|
|
281
283
|
border-radius: 8px;
|
|
282
284
|
cursor: pointer;
|
|
285
|
+
transition: background-color 0.15s ease, color 0.15s ease, transform 0.08s ease;
|
|
283
286
|
}
|
|
284
287
|
|
|
285
288
|
.settings-icon-button:hover {
|
|
@@ -287,6 +290,22 @@ button {
|
|
|
287
290
|
background: var(--settings-hover);
|
|
288
291
|
}
|
|
289
292
|
|
|
293
|
+
.settings-icon-button:active {
|
|
294
|
+
background: var(--settings-active);
|
|
295
|
+
transform: scale(0.95);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.settings-header-actions .settings-icon-button {
|
|
299
|
+
width: 34px;
|
|
300
|
+
height: 34px;
|
|
301
|
+
border-radius: 8px;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.settings-header-actions .settings-icon-button:hover {
|
|
305
|
+
color: var(--settings-text);
|
|
306
|
+
background: var(--settings-hover);
|
|
307
|
+
}
|
|
308
|
+
|
|
290
309
|
.settings-icon-button:focus-visible,
|
|
291
310
|
.settings-nav-button:focus-visible {
|
|
292
311
|
outline: 2px solid var(--settings-focus);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import settingsCss from "./shell.css";
|
|
2
|
-
import accountsCss from "./accounts.css";
|
|
3
|
-
import skinMarketCss from "./skin-market.css";
|
|
2
|
+
import accountsCss from "./accounts.css";
|
|
3
|
+
import skinMarketCss from "./skin-market.css";
|
|
4
|
+
import petMarketCss from "./pet-market.css";
|
|
4
5
|
import {
|
|
5
6
|
RendererSettingsNavigationState,
|
|
6
7
|
RendererSettingsPageScope,
|
|
@@ -69,7 +70,7 @@ export function mountRendererSettingsShell(
|
|
|
69
70
|
root.style.colorScheme = RENDERER_SETTINGS_COLOR_SCHEME;
|
|
70
71
|
const shadow = root.attachShadow({ mode: "open" });
|
|
71
72
|
const style = ownerDocument.createElement("style");
|
|
72
|
-
style.textContent = `${settingsCss}\n${accountsCss}\n${skinMarketCss}`;
|
|
73
|
+
style.textContent = `${settingsCss}\n${accountsCss}\n${skinMarketCss}\n${petMarketCss}`;
|
|
73
74
|
|
|
74
75
|
const dialog = ownerDocument.createElement("dialog");
|
|
75
76
|
dialog.className = "codexhost-settings-dialog";
|