dsh-pet 0.1.5 → 0.1.6

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.
@@ -2,16 +2,21 @@
2
2
  // 说明:浏览器端启动时通过 /pet/config.jsonc 拉取;任何字段缺失/写错都会
3
3
  // 回退到 client.js 的代码默认值,不会导致插件崩溃。
4
4
  {
5
- // 桌宠尺寸(宽度 px;高度自动 = size × 9/16)
6
- "size": 462,
7
- // 桌宠初始位置
8
- // corner: top-left | top-right | bottom-left | bottom-right(默认 bottom-right
9
- // marginX / marginY: 距所属角落对应边缘的偏移(px,相对角落方向)
10
- "position": {
11
- "corner": "top-right",
12
- "marginX": 24,
13
- "marginY": 100
14
- },
5
+ // ===================== 默认宠物列表(多开) =====================
6
+ // pets 数组定义默认宠物;「设置页 → 添加宠物」的默认模板取 pets[0](main)。
7
+ // 每个宠物独立:id(标识)、size(宽度 px,高度自动 = size × 9/16)、
8
+ // position(corner: 四角之一;marginX/marginY: 距所属角落偏移 px
9
+ "pets": [
10
+ {
11
+ "id": "main",
12
+ "size": 462,
13
+ "position": {
14
+ "corner": "top-right",
15
+ "marginX": 24,
16
+ "marginY": 100
17
+ }
18
+ }
19
+ ],
15
20
 
16
21
  // ===================== 动作动画池配置 =====================
17
22
  "animations": {
package/lib/client.js CHANGED
@@ -2,7 +2,8 @@
2
2
  //#region src/client/pickers.ts
3
3
  const pick = (pool, exclude) => {
4
4
  const entries = exclude ? pool.filter((n) => n !== exclude) : pool;
5
- return entries[Math.floor(Math.random() * entries.length)];
5
+ const src = entries.length ? entries : pool;
6
+ return src[Math.floor(Math.random() * src.length)];
6
7
  };
7
8
  const randomBetween = (min, max) => Math.floor(min + Math.random() * (max - min));
8
9
  const pickWeightedCategory = (categories, facing) => {
@@ -19,6 +20,470 @@ const pickWeightedCategory = (categories, facing) => {
19
20
  return eligible[eligible.length - 1];
20
21
  };
21
22
 
23
+ //#endregion
24
+ //#region src/client/settings.ts
25
+ const DEFAULT_PETS = [{
26
+ id: "main",
27
+ size: 462,
28
+ corner: "top-right",
29
+ marginX: 24,
30
+ marginY: 100
31
+ }];
32
+ const petBridge = {
33
+ current: null,
34
+ sync: null,
35
+ template: null
36
+ };
37
+ const NS = "pet.config";
38
+ const zh = {
39
+ nav: "桌宠配置",
40
+ intro: "管理多个桌宠:每个宠物可独立设置大小与位置(保存后即时生效)。",
41
+ petsLabel: "宠物列表",
42
+ add: "添加宠物",
43
+ remove: "删除",
44
+ atLeastOne: "至少保留一个宠物。",
45
+ emptyPets: "暂无宠物,点击「添加宠物」创建。",
46
+ sizeLabel: "大小(宽度 px)",
47
+ sizeHint: "高度自动 = 宽度 × 9/16。",
48
+ cornerLabel: "位置",
49
+ "corner.top-left": "左上角",
50
+ "corner.top-right": "右上角",
51
+ "corner.bottom-left": "左下角",
52
+ "corner.bottom-right": "右下角",
53
+ marginX: "水平偏移",
54
+ marginY: "垂直偏移",
55
+ save: "保存",
56
+ reset: "恢复默认",
57
+ saved: "已保存,桌宠即时生效。",
58
+ loadError: "加载配置失败",
59
+ invalid: "请检查输入:大小需为正数,边距可为任意数字。",
60
+ busy: "保存中…"
61
+ };
62
+ const en = {
63
+ nav: "Pet Config",
64
+ intro: "Manage multiple pets: each pet has its own size and position (applies instantly after saving).",
65
+ petsLabel: "Pets",
66
+ add: "Add pet",
67
+ remove: "Remove",
68
+ atLeastOne: "Keep at least one pet.",
69
+ emptyPets: "No pets yet — click \"Add pet\" to create one.",
70
+ sizeLabel: "Size (width px)",
71
+ sizeHint: "Height is automatic = width × 9/16.",
72
+ cornerLabel: "Position",
73
+ "corner.top-left": "Top-left",
74
+ "corner.top-right": "Top-right",
75
+ "corner.bottom-left": "Bottom-left",
76
+ "corner.bottom-right": "Bottom-right",
77
+ marginX: "Horizontal offset",
78
+ marginY: "Vertical offset",
79
+ save: "Save",
80
+ reset: "Reset to default",
81
+ saved: "Saved — the pets updated instantly.",
82
+ loadError: "Failed to load config",
83
+ invalid: "Check your input: size must be positive; margins can be any number.",
84
+ busy: "Saving…"
85
+ };
86
+ function makePetConfigSection(rt) {
87
+ const { h, useState, t } = rt;
88
+ const CORNERS$1 = [
89
+ "top-left",
90
+ "top-right",
91
+ "bottom-left",
92
+ "bottom-right"
93
+ ];
94
+ const cornerLabel = (c) => t("corner." + c);
95
+ const inputStyle = {
96
+ boxSizing: "border-box",
97
+ border: "1px solid var(--dsw-alias-border-l2)",
98
+ borderRadius: "8px",
99
+ background: "var(--dsw-alias-bg-layer-1)",
100
+ color: "var(--dsw-alias-label-primary)",
101
+ padding: "5px 10px",
102
+ fontSize: "13px",
103
+ minHeight: "28px",
104
+ outline: "none"
105
+ };
106
+ const stripJsonc$1 = (src) => src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^\\:])\/\/.*$/gm, "$1").trim();
107
+ /** 生成一个未占用的宠物 id(pet-2、pet-3…) */
108
+ const nextId = (list) => {
109
+ let n = 2;
110
+ for (;; n++) {
111
+ const id = "pet-" + n;
112
+ if (!list.some((p) => p.id === id)) return id;
113
+ }
114
+ };
115
+ const normalizePet$1 = (p) => {
116
+ const pos = p && p.position && typeof p.position === "object" ? p.position : {};
117
+ return {
118
+ id: String(p && p.id ? p.id : "main"),
119
+ size: Number(p && p.size) > 0 ? Number(p.size) : DEFAULT_PETS[0].size,
120
+ corner: CORNERS$1.includes(pos.corner) ? pos.corner : DEFAULT_PETS[0].corner,
121
+ marginX: Number.isFinite(Number(pos.marginX)) ? Number(pos.marginX) : DEFAULT_PETS[0].marginX,
122
+ marginY: Number.isFinite(Number(pos.marginY)) ? Number(pos.marginY) : DEFAULT_PETS[0].marginY
123
+ };
124
+ };
125
+ /** 从 config.jsonc 对象提取默认宠物列表:必须为 pets 数组;缺失/为空回落代码兜底 DEFAULT_PETS */
126
+ const defaultsFrom = (obj) => {
127
+ const arr = obj && Array.isArray(obj.pets) ? obj.pets.filter((p) => p && p.id).map(normalizePet$1) : [];
128
+ return arr.length ? arr : DEFAULT_PETS.map((p) => ({ ...p }));
129
+ };
130
+ return function PetConfigSection() {
131
+ const initPets = petBridge.current ?? DEFAULT_PETS;
132
+ const [pets, setPets] = useState(initPets.map((p) => ({ ...p })));
133
+ const [selId, setSelId] = useState(initPets[0]?.id ?? "");
134
+ const [busy, setBusy] = useState(false);
135
+ const [msg, setMsg] = useState({
136
+ kind: "",
137
+ text: ""
138
+ });
139
+ const sel = pets.find((p) => p.id === selId) ?? pets[0] ?? null;
140
+ const updateSel = (patch) => setPets((list) => list.map((p) => p.id === (sel?.id ?? "") ? {
141
+ ...p,
142
+ ...patch
143
+ } : p));
144
+ const validated = () => {
145
+ for (const p of pets) if (!Number.isFinite(p.size) || p.size <= 0 || !Number.isFinite(p.marginX) || !Number.isFinite(p.marginY)) {
146
+ setMsg({
147
+ kind: "err",
148
+ text: t("invalid")
149
+ });
150
+ return null;
151
+ }
152
+ return pets;
153
+ };
154
+ const save = async () => {
155
+ const list = validated();
156
+ if (!list) return;
157
+ setBusy(true);
158
+ setMsg({
159
+ kind: "",
160
+ text: ""
161
+ });
162
+ try {
163
+ const res = await fetch("/pet/config", {
164
+ method: "PUT",
165
+ headers: { "content-type": "application/json" },
166
+ body: JSON.stringify({ pets: list.map((p) => ({
167
+ id: p.id,
168
+ size: p.size,
169
+ position: {
170
+ corner: p.corner,
171
+ marginX: p.marginX,
172
+ marginY: p.marginY
173
+ }
174
+ })) })
175
+ });
176
+ if (!res.ok) throw new Error("HTTP " + res.status);
177
+ petBridge.current = list;
178
+ petBridge.sync?.(list);
179
+ setMsg({
180
+ kind: "ok",
181
+ text: t("saved")
182
+ });
183
+ } catch {
184
+ setMsg({
185
+ kind: "err",
186
+ text: t("loadError")
187
+ });
188
+ } finally {
189
+ setBusy(false);
190
+ }
191
+ };
192
+ const reset = async () => {
193
+ setBusy(true);
194
+ setMsg({
195
+ kind: "",
196
+ text: ""
197
+ });
198
+ try {
199
+ await fetch("/pet/config", { method: "DELETE" });
200
+ const defRes = await fetch("/pet/config.jsonc");
201
+ const obj = JSON.parse(stripJsonc$1(await defRes.text()));
202
+ const defs = defaultsFrom(obj);
203
+ setPets(defs.map((p) => ({ ...p })));
204
+ setSelId(defs[0]?.id ?? "");
205
+ petBridge.current = defs;
206
+ petBridge.sync?.(defs);
207
+ setMsg({
208
+ kind: "ok",
209
+ text: t("saved")
210
+ });
211
+ } catch {
212
+ setMsg({
213
+ kind: "err",
214
+ text: t("loadError")
215
+ });
216
+ } finally {
217
+ setBusy(false);
218
+ }
219
+ };
220
+ const addPet = () => {
221
+ const id = nextId(pets);
222
+ const tpl = petBridge.template ?? DEFAULT_PETS[0];
223
+ const np = {
224
+ id,
225
+ size: tpl.size,
226
+ corner: tpl.corner,
227
+ marginX: tpl.marginX,
228
+ marginY: tpl.marginY
229
+ };
230
+ setPets((list) => [...list, np]);
231
+ setSelId(id);
232
+ };
233
+ const removeSel = () => {
234
+ if (pets.length <= 1) {
235
+ setMsg({
236
+ kind: "err",
237
+ text: t("atLeastOne")
238
+ });
239
+ return;
240
+ }
241
+ const list = pets.filter((p) => p.id !== (sel?.id ?? ""));
242
+ setPets(list);
243
+ setSelId(list[0].id);
244
+ };
245
+ const field = (key, value, setter, width) => h("input", {
246
+ type: "number",
247
+ step: key === "size" ? "10" : "1",
248
+ min: key === "size" ? "120" : "",
249
+ value: String(value),
250
+ disabled: busy || !sel,
251
+ onChange: (e) => setter(Number(e.target.value)),
252
+ style: {
253
+ width,
254
+ ...inputStyle
255
+ }
256
+ });
257
+ return h("section", {
258
+ style: {
259
+ maxWidth: "720px",
260
+ color: "var(--dsw-alias-label-primary)",
261
+ display: "flex",
262
+ flexDirection: "column",
263
+ gap: "6px"
264
+ },
265
+ children: [
266
+ h("h2", {
267
+ style: {
268
+ margin: 0,
269
+ fontSize: "16px",
270
+ fontWeight: 500,
271
+ lineHeight: "24px"
272
+ },
273
+ children: t("nav")
274
+ }),
275
+ h("p", {
276
+ style: {
277
+ margin: 0,
278
+ fontSize: "14px",
279
+ color: "var(--dsw-alias-label-tertiary)",
280
+ lineHeight: "22px"
281
+ },
282
+ children: t("intro")
283
+ }),
284
+ h("div", {
285
+ style: {
286
+ display: "flex",
287
+ gap: "8px",
288
+ flexWrap: "wrap",
289
+ alignItems: "center",
290
+ marginTop: "4px"
291
+ },
292
+ children: [
293
+ h("span", {
294
+ style: {
295
+ fontSize: "12px",
296
+ color: "var(--dsw-alias-label-secondary)"
297
+ },
298
+ children: t("petsLabel")
299
+ }),
300
+ ...pets.map((p) => h("button", {
301
+ key: p.id,
302
+ type: "button",
303
+ onClick: () => setSelId(p.id),
304
+ style: {
305
+ border: "1px solid " + (p.id === (sel?.id ?? "") ? "var(--dsw-alias-state-business-primary)" : "var(--dsw-alias-border-l2)"),
306
+ background: p.id === (sel?.id ?? "") ? "var(--dsw-alias-interactive-bg-active)" : "transparent",
307
+ color: "var(--dsw-alias-label-primary)",
308
+ borderRadius: "8px",
309
+ padding: "4px 12px",
310
+ fontSize: "13px",
311
+ cursor: "pointer"
312
+ },
313
+ children: p.id + " (" + p.size + "px)"
314
+ })),
315
+ h("button", {
316
+ type: "button",
317
+ onClick: addPet,
318
+ disabled: busy,
319
+ style: {
320
+ border: "1px dashed var(--dsw-alias-border-l2)",
321
+ background: "transparent",
322
+ color: "var(--dsw-alias-label-secondary)",
323
+ borderRadius: "8px",
324
+ padding: "4px 12px",
325
+ fontSize: "13px",
326
+ cursor: "pointer"
327
+ },
328
+ children: "+ " + t("add")
329
+ })
330
+ ]
331
+ }),
332
+ sel ? h("div", {
333
+ style: {
334
+ display: "flex",
335
+ gap: "16px",
336
+ flexWrap: "wrap",
337
+ marginTop: "8px",
338
+ padding: "12px 14px",
339
+ border: "1px solid var(--dsw-alias-border-l2)",
340
+ borderRadius: "12px"
341
+ },
342
+ children: [
343
+ h("label", {
344
+ style: {
345
+ display: "flex",
346
+ flexDirection: "column",
347
+ gap: "4px",
348
+ fontSize: "12px",
349
+ color: "var(--dsw-alias-label-secondary)"
350
+ },
351
+ children: [
352
+ t("sizeLabel"),
353
+ field("size", sel.size, (v) => updateSel({ size: v }), "150px"),
354
+ h("span", {
355
+ style: {
356
+ fontSize: "11px",
357
+ color: "var(--dsw-alias-label-tertiary)"
358
+ },
359
+ children: t("sizeHint")
360
+ })
361
+ ]
362
+ }),
363
+ h("label", {
364
+ style: {
365
+ display: "flex",
366
+ flexDirection: "column",
367
+ gap: "4px",
368
+ fontSize: "12px",
369
+ color: "var(--dsw-alias-label-secondary)"
370
+ },
371
+ children: [t("cornerLabel"), h("select", {
372
+ value: sel.corner,
373
+ disabled: busy,
374
+ onChange: (e) => updateSel({ corner: e.target.value }),
375
+ style: {
376
+ width: "160px",
377
+ ...inputStyle
378
+ },
379
+ children: CORNERS$1.map((c) => h("option", {
380
+ key: c,
381
+ value: c,
382
+ children: cornerLabel(c)
383
+ }))
384
+ })]
385
+ }),
386
+ h("label", {
387
+ style: {
388
+ display: "flex",
389
+ flexDirection: "column",
390
+ gap: "4px",
391
+ fontSize: "12px",
392
+ color: "var(--dsw-alias-label-secondary)"
393
+ },
394
+ children: [t("marginX"), field("marginX", sel.marginX, (v) => updateSel({ marginX: v }), "120px")]
395
+ }),
396
+ h("label", {
397
+ style: {
398
+ display: "flex",
399
+ flexDirection: "column",
400
+ gap: "4px",
401
+ fontSize: "12px",
402
+ color: "var(--dsw-alias-label-secondary)"
403
+ },
404
+ children: [t("marginY"), field("marginY", sel.marginY, (v) => updateSel({ marginY: v }), "120px")]
405
+ }),
406
+ h("button", {
407
+ type: "button",
408
+ onClick: removeSel,
409
+ disabled: busy,
410
+ title: t("remove"),
411
+ style: {
412
+ alignSelf: "flex-end",
413
+ border: "1px solid var(--dsw-alias-state-error-secondary)",
414
+ background: "transparent",
415
+ color: "var(--dsw-alias-state-error-primary)",
416
+ borderRadius: "8px",
417
+ padding: "4px 12px",
418
+ fontSize: "12px",
419
+ cursor: "pointer"
420
+ },
421
+ children: t("remove")
422
+ })
423
+ ]
424
+ }) : h("p", {
425
+ style: {
426
+ margin: 0,
427
+ fontSize: "13px",
428
+ color: "var(--dsw-alias-label-tertiary)"
429
+ },
430
+ children: t("emptyPets")
431
+ }),
432
+ h("div", {
433
+ style: {
434
+ display: "flex",
435
+ gap: "8px",
436
+ alignItems: "center",
437
+ marginTop: "4px"
438
+ },
439
+ children: [
440
+ h("button", {
441
+ type: "button",
442
+ disabled: busy,
443
+ onClick: save,
444
+ style: {
445
+ border: "1px solid var(--dsw-alias-button-info-fill)",
446
+ background: "var(--dsw-alias-button-info-fill)",
447
+ color: "#fff",
448
+ borderRadius: "8px",
449
+ padding: "4px 14px",
450
+ fontSize: "12px",
451
+ cursor: "pointer",
452
+ opacity: busy ? .5 : 1
453
+ },
454
+ children: t("save")
455
+ }),
456
+ h("button", {
457
+ type: "button",
458
+ disabled: busy,
459
+ onClick: reset,
460
+ style: {
461
+ border: "1px solid var(--dsw-alias-border-l2)",
462
+ background: "transparent",
463
+ color: "var(--dsw-alias-label-primary)",
464
+ borderRadius: "8px",
465
+ padding: "4px 14px",
466
+ fontSize: "12px",
467
+ cursor: "pointer",
468
+ opacity: busy ? .5 : 1
469
+ },
470
+ children: t("reset")
471
+ }),
472
+ msg.text ? h("span", {
473
+ style: {
474
+ fontSize: "12px",
475
+ color: msg.kind === "err" ? "var(--dsw-alias-state-error-primary)" : "var(--dsw-alias-state-ok-primary)",
476
+ marginLeft: "4px"
477
+ },
478
+ children: msg.text
479
+ }) : null
480
+ ]
481
+ })
482
+ ]
483
+ });
484
+ };
485
+ }
486
+
22
487
  //#endregion
23
488
  //#region src/client/config.ts
24
489
  const FALLBACK_IDLE = "待机呼吸休闲";
@@ -97,6 +562,43 @@ const buildAnim = (obj) => {
97
562
  }
98
563
  };
99
564
  };
565
+ const CORNERS = [
566
+ "top-left",
567
+ "top-right",
568
+ "bottom-left",
569
+ "bottom-right"
570
+ ];
571
+ function normalizePet(p) {
572
+ const pos = p && p.position && typeof p.position === "object" ? p.position : {};
573
+ return {
574
+ id: String(p && p.id ? p.id : "main"),
575
+ size: Number(p && p.size) > 0 ? Number(p.size) : DEFAULT_PETS[0].size,
576
+ corner: CORNERS.indexOf(pos.corner) !== -1 ? pos.corner : DEFAULT_PETS[0].corner,
577
+ marginX: Number.isFinite(Number(pos.marginX)) ? Number(pos.marginX) : DEFAULT_PETS[0].marginX,
578
+ marginY: Number.isFinite(Number(pos.marginY)) ? Number(pos.marginY) : DEFAULT_PETS[0].marginY
579
+ };
580
+ }
581
+ function extractDefaultPets(obj) {
582
+ const arr = obj && Array.isArray(obj.pets) ? obj.pets.filter((p) => p && p.id) : [];
583
+ if (!arr.length) return DEFAULT_PETS.map((p) => ({ ...p }));
584
+ const seen = new Set();
585
+ const out = [];
586
+ for (const p of arr) {
587
+ const id = String(p.id);
588
+ if (!seen.has(id)) {
589
+ seen.add(id);
590
+ out.push(normalizePet(p));
591
+ }
592
+ }
593
+ return out.length ? out : DEFAULT_PETS.map((p) => ({ ...p }));
594
+ }
595
+ function resolvePets(defaults, user) {
596
+ if (user && Array.isArray(user.pets)) {
597
+ const list = user.pets.filter((p) => p && p.id).map(normalizePet);
598
+ return list.length ? list : defaults;
599
+ }
600
+ return defaults;
601
+ }
100
602
 
101
603
  //#endregion
102
604
  //#region src/client/constants.ts
@@ -112,13 +614,6 @@ const DRAG_THRESHOLD = 5;
112
614
 
113
615
  //#endregion
114
616
  //#region src/client/app.ts
115
- /** 支持的角落白名单 */
116
- const CORNERS = [
117
- "top-left",
118
- "top-right",
119
- "bottom-left",
120
- "bottom-right"
121
- ];
122
617
  function makeFactory() {
123
618
  return (require) => {
124
619
  var module = { exports: {} };
@@ -147,8 +642,8 @@ function makeFactory() {
147
642
  document.head.appendChild(tag);
148
643
  }
149
644
  let ANIM = buildMinimalAnim();
150
- function Pet() {
151
- const [size, setSize] = useState(462);
645
+ function PetCard({ cfg }) {
646
+ const [size, setSize] = useState(cfg.size);
152
647
  const halfW = size / 2;
153
648
  const halfH = size * 9 / 16 / 2;
154
649
  const [anim, setAnim] = useState(ANIM.idle[0] ?? "");
@@ -156,40 +651,24 @@ function makeFactory() {
156
651
  const [facing, setFacing] = useState("left");
157
652
  const [dragging, setDragging] = useState(false);
158
653
  const [customPos, setCustomPos] = useState(null);
159
- const [corner, setCorner] = useState("bottom-right");
654
+ const [corner, setCorner] = useState(cfg.corner);
160
655
  const [margin, setMargin] = useState({
161
- x: 24,
162
- y: 0
656
+ x: cfg.marginX,
657
+ y: cfg.marginY
163
658
  });
164
659
  useEffect(() => {
165
- fetch("/pet/config.jsonc").then((r) => {
166
- if (!r.ok) throw new Error("config");
167
- return r.text();
168
- }).then((src) => {
169
- const obj = JSON.parse(stripJsonc(src));
170
- const sz = Number(obj && obj.size);
171
- if (Number.isFinite(sz) && sz > 0) setSize(sz);
172
- ANIM = buildAnim(obj);
173
- const p = obj && obj.position;
174
- if (p) {
175
- if (CORNERS.indexOf(p.corner) !== -1) setCorner(p.corner);
176
- const mx = Number(p.marginX);
177
- const my = Number(p.marginY);
178
- if (!Number.isNaN(mx)) setMargin((m) => ({
179
- ...m,
180
- x: mx
181
- }));
182
- if (!Number.isNaN(my)) setMargin((m) => ({
183
- ...m,
184
- y: my
185
- }));
186
- }
187
- if (ANIM.idle && ANIM.idle.length) {
188
- setAnim(ANIM.idle[0]);
189
- setSeq((s) => s + 1);
190
- }
191
- }).catch(() => {});
192
- }, []);
660
+ setSize(cfg.size);
661
+ setCorner(cfg.corner);
662
+ setMargin({
663
+ x: cfg.marginX,
664
+ y: cfg.marginY
665
+ });
666
+ }, [
667
+ cfg.size,
668
+ cfg.corner,
669
+ cfg.marginX,
670
+ cfg.marginY
671
+ ]);
193
672
  const [seq, setSeq] = useState(0);
194
673
  const rootRef = useRef(null);
195
674
  const stageRef = useRef(null);
@@ -538,15 +1017,73 @@ function makeFactory() {
538
1017
  })
539
1018
  });
540
1019
  }
1020
+ function PetMulti() {
1021
+ const [pets, setPets] = useState([]);
1022
+ const [ready, setReady] = useState(false);
1023
+ useEffect(() => {
1024
+ let alive = true;
1025
+ (async () => {
1026
+ try {
1027
+ const r1 = await fetch("/pet/config.jsonc");
1028
+ if (!r1.ok) return;
1029
+ const obj = JSON.parse(stripJsonc(await r1.text()));
1030
+ ANIM = buildAnim(obj);
1031
+ const defaults = extractDefaultPets(obj);
1032
+ let user = {};
1033
+ try {
1034
+ const r2 = await fetch("/pet/config");
1035
+ if (r2.ok && r2.status !== 204) user = await r2.json().catch(() => ({}));
1036
+ } catch {}
1037
+ const merged = resolvePets(defaults, user);
1038
+ if (!alive) return;
1039
+ petBridge.current = merged;
1040
+ petBridge.template = defaults.length ? defaults[0] : null;
1041
+ petBridge.sync = (list) => {
1042
+ setPets(list);
1043
+ petBridge.current = list;
1044
+ };
1045
+ setPets(merged);
1046
+ setReady(true);
1047
+ } catch {}
1048
+ })();
1049
+ return () => {
1050
+ alive = false;
1051
+ petBridge.sync = null;
1052
+ };
1053
+ }, []);
1054
+ return ready ? pets.map((p) => h(PetCard, {
1055
+ key: p.id,
1056
+ cfg: p
1057
+ })) : null;
1058
+ }
541
1059
  const name = "pet";
542
- const inject = ["slots"];
1060
+ const inject = ["slots", "locale"];
543
1061
  function apply(ctx, _config) {
1062
+ ctx.effect(() => ctx.locale.register(NS, {
1063
+ zh,
1064
+ en
1065
+ }), "dsh-pet: dictionaries");
1066
+ const t = ctx.locale.bind(NS);
544
1067
  ctx.slots.inject("shell.overlay", function* () {
545
1068
  yield ctx.slots.register({
546
1069
  name: "shell.overlay",
547
1070
  id: "pet",
548
1071
  order: 1e3
549
- }, () => h(Pet, {}));
1072
+ }, () => h(PetMulti, {}));
1073
+ });
1074
+ const PetConfigSection = makePetConfigSection({
1075
+ h,
1076
+ useState,
1077
+ t
1078
+ });
1079
+ ctx.slots.inject("settings.section", function* () {
1080
+ yield ctx.slots.register({
1081
+ name: "settings.section",
1082
+ id: "pet-config",
1083
+ order: 30,
1084
+ label: () => t("nav"),
1085
+ inject: () => ({ t })
1086
+ }, PetConfigSection);
550
1087
  });
551
1088
  }
552
1089
  module.exports = {