@linxin666/dsh-pet 0.1.14 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +73 -38
- package/README.zh.md +91 -55
- package/assets/whale/pet.json +12 -1
- package/cordis.patch.yml +4 -4
- package/lib/client.js +296 -347
- package/lib/client.js.map +1 -1
- package/lib/index.js +975 -317
- package/lib/invariant.js +1 -1
- package/lib/{state-P-wsVJ6O.js → state-CFyJv0sQ.js} +15 -1
- package/lib/types/affinity.d.ts +15 -0
- package/lib/types/affinity.d.ts.map +1 -1
- package/lib/types/affinity.js +14 -0
- package/lib/types/client/PetDockEntry.d.ts +14 -12
- package/lib/types/client/PetDockEntry.d.ts.map +1 -1
- package/lib/types/client/PetDockEntry.js +13 -8
- package/lib/types/client/PetSettingsCard.d.ts +22 -9
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +42 -7
- package/lib/types/client/{WhalePet.d.ts → PetSprite.d.ts} +21 -20
- package/lib/types/client/PetSprite.d.ts.map +1 -0
- package/lib/types/client/{WhalePet.js → PetSprite.js} +44 -67
- package/lib/types/client/PluginSettingsCard.d.ts +26 -11
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +30 -8
- package/lib/types/client/index.d.ts +11 -8
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +32 -14
- package/lib/types/client/locales.d.ts +10 -10
- package/lib/types/client/locales.js +10 -10
- package/lib/types/client/pet-store.d.ts +5 -0
- package/lib/types/client/pet-store.d.ts.map +1 -1
- package/lib/types/client/pet-store.js +4 -0
- package/lib/types/client/settings-form.d.ts +76 -5
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +97 -20
- package/lib/types/client/spritesheet.d.ts +14 -48
- package/lib/types/client/spritesheet.d.ts.map +1 -1
- package/lib/types/client/spritesheet.js +13 -81
- package/lib/types/dsh-home.d.ts +17 -0
- package/lib/types/dsh-home.d.ts.map +1 -0
- package/lib/types/dsh-home.js +34 -0
- package/lib/types/event-projection.d.ts +36 -0
- package/lib/types/event-projection.d.ts.map +1 -0
- package/lib/types/event-projection.js +98 -0
- package/lib/types/index.d.ts +24 -11
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +53 -33
- package/lib/types/ledger.d.ts +79 -0
- package/lib/types/ledger.d.ts.map +1 -0
- package/lib/types/ledger.js +146 -0
- package/lib/types/persist.d.ts +15 -4
- package/lib/types/persist.d.ts.map +1 -1
- package/lib/types/persist.js +39 -8
- package/lib/types/registry.d.ts +158 -0
- package/lib/types/registry.d.ts.map +1 -0
- package/lib/types/registry.js +276 -0
- package/lib/types/routes.d.ts +10 -10
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +169 -51
- package/lib/types/service.d.ts +59 -57
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +111 -211
- package/package.json +2 -2
- package/src/affinity.ts +33 -0
- package/src/client/PetDockEntry.tsx +34 -26
- package/src/client/PetSettingsCard.tsx +67 -22
- package/src/client/{WhalePet.test.tsx → PetSprite.test.tsx} +56 -8
- package/src/client/{WhalePet.tsx → PetSprite.tsx} +55 -73
- package/src/client/PluginSettingsCard.tsx +90 -18
- package/src/client/index.ts +38 -16
- package/src/client/locales.ts +10 -10
- package/src/client/pet-store.ts +9 -0
- package/src/client/settings-card.module.css +9 -0
- package/src/client/settings-form.ts +153 -19
- package/src/client/spritesheet.ts +16 -92
- package/src/dsh-home.test.ts +35 -0
- package/src/dsh-home.ts +36 -0
- package/src/event-projection.ts +128 -0
- package/src/index.test.ts +23 -0
- package/src/index.ts +89 -45
- package/src/ledger.test.ts +69 -0
- package/src/ledger.ts +190 -0
- package/src/persist.test.ts +52 -3
- package/src/persist.ts +49 -11
- package/src/registry.test.ts +132 -0
- package/src/registry.ts +400 -0
- package/src/routes.ts +166 -53
- package/src/service.ts +153 -285
- package/lib/types/client/WhalePet.d.ts.map +0 -1
package/lib/client.js
CHANGED
|
@@ -22,6 +22,7 @@ window.__ModuleLoader__.load({
|
|
|
22
22
|
return (0, _deepseek_ai_dsh_client_runtime_client.defineStore)({
|
|
23
23
|
init: () => ({
|
|
24
24
|
snapshot: null,
|
|
25
|
+
pets: [],
|
|
25
26
|
state: "loading",
|
|
26
27
|
error: null,
|
|
27
28
|
feedback: null
|
|
@@ -32,6 +33,9 @@ window.__ModuleLoader__.load({
|
|
|
32
33
|
draft.state = "ready";
|
|
33
34
|
draft.error = null;
|
|
34
35
|
},
|
|
36
|
+
setPets: (draft, pets) => {
|
|
37
|
+
draft.pets = pets;
|
|
38
|
+
},
|
|
35
39
|
setState: (draft, state, error) => {
|
|
36
40
|
draft.state = state;
|
|
37
41
|
draft.error = error;
|
|
@@ -42,194 +46,24 @@ window.__ModuleLoader__.load({
|
|
|
42
46
|
}
|
|
43
47
|
});
|
|
44
48
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
400,
|
|
64
|
-
500,
|
|
65
|
-
400,
|
|
66
|
-
400,
|
|
67
|
-
500
|
|
68
|
-
],
|
|
69
|
-
loop: true
|
|
70
|
-
},
|
|
71
|
-
"running-right": {
|
|
72
|
-
frames: [
|
|
73
|
-
0,
|
|
74
|
-
1,
|
|
75
|
-
2,
|
|
76
|
-
3,
|
|
77
|
-
4,
|
|
78
|
-
5,
|
|
79
|
-
6,
|
|
80
|
-
7
|
|
81
|
-
],
|
|
82
|
-
durations: [
|
|
83
|
-
225,
|
|
84
|
-
225,
|
|
85
|
-
225,
|
|
86
|
-
225,
|
|
87
|
-
225,
|
|
88
|
-
225,
|
|
89
|
-
225,
|
|
90
|
-
225
|
|
91
|
-
],
|
|
92
|
-
loop: true
|
|
93
|
-
},
|
|
94
|
-
"running-left": {
|
|
95
|
-
frames: [
|
|
96
|
-
0,
|
|
97
|
-
1,
|
|
98
|
-
2,
|
|
99
|
-
3,
|
|
100
|
-
4,
|
|
101
|
-
5,
|
|
102
|
-
6,
|
|
103
|
-
7
|
|
104
|
-
],
|
|
105
|
-
durations: [
|
|
106
|
-
225,
|
|
107
|
-
225,
|
|
108
|
-
225,
|
|
109
|
-
225,
|
|
110
|
-
225,
|
|
111
|
-
225,
|
|
112
|
-
225,
|
|
113
|
-
225
|
|
114
|
-
],
|
|
115
|
-
loop: true
|
|
116
|
-
},
|
|
117
|
-
waving: {
|
|
118
|
-
frames: [
|
|
119
|
-
0,
|
|
120
|
-
1,
|
|
121
|
-
2,
|
|
122
|
-
3
|
|
123
|
-
],
|
|
124
|
-
durations: [
|
|
125
|
-
350,
|
|
126
|
-
350,
|
|
127
|
-
350,
|
|
128
|
-
350
|
|
129
|
-
],
|
|
130
|
-
loop: true
|
|
131
|
-
},
|
|
132
|
-
jumping: {
|
|
133
|
-
frames: [
|
|
134
|
-
0,
|
|
135
|
-
1,
|
|
136
|
-
2,
|
|
137
|
-
3,
|
|
138
|
-
4
|
|
139
|
-
],
|
|
140
|
-
durations: [
|
|
141
|
-
300,
|
|
142
|
-
300,
|
|
143
|
-
300,
|
|
144
|
-
350,
|
|
145
|
-
350
|
|
146
|
-
],
|
|
147
|
-
loop: false,
|
|
148
|
-
fallback: "idle"
|
|
149
|
-
},
|
|
150
|
-
failed: {
|
|
151
|
-
frames: [
|
|
152
|
-
0,
|
|
153
|
-
1,
|
|
154
|
-
2,
|
|
155
|
-
3,
|
|
156
|
-
4,
|
|
157
|
-
5,
|
|
158
|
-
6,
|
|
159
|
-
7
|
|
160
|
-
],
|
|
161
|
-
durations: [
|
|
162
|
-
450,
|
|
163
|
-
450,
|
|
164
|
-
450,
|
|
165
|
-
500,
|
|
166
|
-
550,
|
|
167
|
-
600,
|
|
168
|
-
450,
|
|
169
|
-
450
|
|
170
|
-
],
|
|
171
|
-
loop: false,
|
|
172
|
-
fallback: "idle"
|
|
173
|
-
},
|
|
174
|
-
waiting: {
|
|
175
|
-
frames: [
|
|
176
|
-
0,
|
|
177
|
-
1,
|
|
178
|
-
2,
|
|
179
|
-
3,
|
|
180
|
-
4,
|
|
181
|
-
5
|
|
182
|
-
],
|
|
183
|
-
durations: [
|
|
184
|
-
450,
|
|
185
|
-
450,
|
|
186
|
-
500,
|
|
187
|
-
450,
|
|
188
|
-
450,
|
|
189
|
-
500
|
|
190
|
-
],
|
|
191
|
-
loop: true
|
|
192
|
-
},
|
|
193
|
-
running: {
|
|
194
|
-
frames: [
|
|
195
|
-
0,
|
|
196
|
-
1,
|
|
197
|
-
2,
|
|
198
|
-
3,
|
|
199
|
-
4,
|
|
200
|
-
5
|
|
201
|
-
],
|
|
202
|
-
durations: [
|
|
203
|
-
250,
|
|
204
|
-
250,
|
|
205
|
-
250,
|
|
206
|
-
250,
|
|
207
|
-
250,
|
|
208
|
-
250
|
|
209
|
-
],
|
|
210
|
-
loop: true
|
|
211
|
-
},
|
|
212
|
-
review: {
|
|
213
|
-
frames: [
|
|
214
|
-
0,
|
|
215
|
-
1,
|
|
216
|
-
2,
|
|
217
|
-
3,
|
|
218
|
-
4,
|
|
219
|
-
5
|
|
220
|
-
],
|
|
221
|
-
durations: [
|
|
222
|
-
550,
|
|
223
|
-
550,
|
|
224
|
-
550,
|
|
225
|
-
550,
|
|
226
|
-
550,
|
|
227
|
-
550
|
|
228
|
-
],
|
|
229
|
-
loop: true
|
|
230
|
-
}
|
|
231
|
-
};
|
|
232
|
-
/** Row index of one animation track (mirrors state.ts rowOf). */
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region ../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
51
|
+
function r(e) {
|
|
52
|
+
var t, f, n = "";
|
|
53
|
+
if ("string" == typeof e || "number" == typeof e) n += e;
|
|
54
|
+
else if ("object" == typeof e) if (Array.isArray(e)) {
|
|
55
|
+
var o = e.length;
|
|
56
|
+
for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
|
|
57
|
+
} else for (f in e) e[f] && (n && (n += " "), n += f);
|
|
58
|
+
return n;
|
|
59
|
+
}
|
|
60
|
+
function clsx() {
|
|
61
|
+
for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
|
|
62
|
+
return n;
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/client/spritesheet.ts
|
|
66
|
+
/** Row index of one animation track (the fixed 9-row contract). */
|
|
233
67
|
function rowOfTrack(animation) {
|
|
234
68
|
return {
|
|
235
69
|
idle: 0,
|
|
@@ -250,52 +84,20 @@ window.__ModuleLoader__.load({
|
|
|
250
84
|
* atlas coordinates here would drift each frame by the scale factor and
|
|
251
85
|
* render torn/overlapping frames.
|
|
252
86
|
*/
|
|
253
|
-
function framePosition(row, col, scale = 1) {
|
|
87
|
+
function framePosition(cell, columns, row, col, scale = 1) {
|
|
254
88
|
return {
|
|
255
|
-
x: -col *
|
|
256
|
-
y: -row *
|
|
89
|
+
x: -col * cell.width * scale,
|
|
90
|
+
y: -row * cell.height * scale
|
|
257
91
|
};
|
|
258
92
|
}
|
|
259
93
|
/**
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
* @param image - the fully decoded spritesheet (natural size 1536×1872).
|
|
265
|
-
* @returns per-row frame counts, length 9.
|
|
266
|
-
*/
|
|
267
|
-
function detectFrameCounts(image) {
|
|
268
|
-
const canvas = document.createElement("canvas");
|
|
269
|
-
canvas.width = image.naturalWidth;
|
|
270
|
-
canvas.height = image.naturalHeight;
|
|
271
|
-
const ctx = canvas.getContext("2d");
|
|
272
|
-
if (ctx === null) return Array.from({ length: 9 }, () => 8);
|
|
273
|
-
ctx.drawImage(image, 0, 0);
|
|
274
|
-
const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
275
|
-
const counts = [];
|
|
276
|
-
const stride = 1536;
|
|
277
|
-
const probeStep = 8;
|
|
278
|
-
const margin = 12;
|
|
279
|
-
for (let row = 0; row < 9; row++) {
|
|
280
|
-
let count = 0;
|
|
281
|
-
for (let col = 0; col < 8; col++) {
|
|
282
|
-
let hasContent = false;
|
|
283
|
-
const x0 = col * 192;
|
|
284
|
-
const y0 = row * 208;
|
|
285
|
-
for (let y = y0 + margin; y < y0 + 208 - margin && !hasContent; y += probeStep) for (let x = x0 + margin; x < x0 + 192 - margin && !hasContent; x += probeStep) if ((data[(y * stride + x) * 4 + 3] ?? 0) > 8) hasContent = true;
|
|
286
|
-
if (hasContent) count += 1;
|
|
287
|
-
}
|
|
288
|
-
counts.push(count);
|
|
289
|
-
}
|
|
290
|
-
return counts;
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* Trim a track to the actual frame count of its row. A row with 0 detected
|
|
294
|
-
* frames degrades to the first frame (the atlas is still loading or corrupt)
|
|
295
|
-
* so the pet never renders blank.
|
|
94
|
+
* Trim a track to the actual frame count of its row (the manifest's per-row
|
|
95
|
+
* counts are authoritative; this is a last-line guard against a definition
|
|
96
|
+
* whose row count disagrees with its track table). A row with 0 detected
|
|
97
|
+
* frames degrades to the first frame so the pet never renders blank.
|
|
296
98
|
*/
|
|
297
99
|
function trimTrack(track, frameCount) {
|
|
298
|
-
const n = Math.max(1, Math.min(frameCount, track.frames.length));
|
|
100
|
+
const n = Math.max(1, Math.min(frameCount, track.frames.length, track.durations.length));
|
|
299
101
|
return {
|
|
300
102
|
frames: track.frames.slice(0, n),
|
|
301
103
|
durations: track.durations.slice(0, n),
|
|
@@ -332,35 +134,32 @@ window.__ModuleLoader__.load({
|
|
|
332
134
|
"summon": "kz2Bea_summon"
|
|
333
135
|
};
|
|
334
136
|
//#endregion
|
|
335
|
-
//#region src/client/
|
|
137
|
+
//#region src/client/PetSprite.tsx
|
|
336
138
|
/**
|
|
337
|
-
*
|
|
139
|
+
* Pet sprite companion component — the browser half's centerpiece. Renders a
|
|
338
140
|
* fixed-position floating sprite (React portal onto document.body), plays
|
|
339
|
-
* the
|
|
340
|
-
*
|
|
341
|
-
* reposition (persisted via setConfig).
|
|
342
|
-
*
|
|
141
|
+
* the track matching the host animation snapshot, and exposes the
|
|
142
|
+
* interaction surface: click to pet, hover panel with feed/rename/hide, drag
|
|
143
|
+
* to reposition (persisted via setConfig). Everything visual comes from the
|
|
144
|
+
* pet definition the host serves ('/api/pet/pets' + the state snapshot's
|
|
145
|
+
* pet id), so one component renders every registry entry.
|
|
146
|
+
* @module @linxin666/dsh-pet/client/PetSprite
|
|
343
147
|
*/
|
|
344
|
-
/** Browser URL of the whale-girl atlas (served by the host half's own route). */
|
|
345
|
-
const PET_SPRITESHEET_URL = "/pet/whale/spritesheet.webp";
|
|
346
|
-
/** Browser URL of the whale-girl manifest (authoritative per-row frame counts). */
|
|
347
|
-
const PET_MANIFEST_URL = "/pet/whale/pet.json";
|
|
348
148
|
/** Clamp a drag offset inside the viewport with a margin. */
|
|
349
149
|
function clampOffset(value, max) {
|
|
350
150
|
return Math.max(0, Math.min(max, value));
|
|
351
151
|
}
|
|
352
152
|
/**
|
|
353
153
|
* The floating pet. The spritesheet frame advances on requestAnimationFrame
|
|
354
|
-
* with per-frame durations from
|
|
355
|
-
* the background position is written straight to the sprite
|
|
356
|
-
* per-frame React state).
|
|
154
|
+
* with per-frame durations from the definition's tracks; the atlas image is
|
|
155
|
+
* loaded once and the background position is written straight to the sprite
|
|
156
|
+
* element (no per-frame React state).
|
|
357
157
|
*/
|
|
358
|
-
function
|
|
359
|
-
const { snapshot, display, feedback } = props;
|
|
158
|
+
function PetSprite(props) {
|
|
159
|
+
const { snapshot, definition, display, feedback } = props;
|
|
360
160
|
const spriteRef = (0, react.useRef)(null);
|
|
361
161
|
const floatRef = (0, react.useRef)(null);
|
|
362
162
|
const [imageReady, setImageReady] = (0, react.useState)(false);
|
|
363
|
-
const [frameCounts, setFrameCounts] = (0, react.useState)(null);
|
|
364
163
|
const [hovered, setHovered] = (0, react.useState)(false);
|
|
365
164
|
const [renaming, setRenaming] = (0, react.useState)(false);
|
|
366
165
|
const [nameDraft, setNameDraft] = (0, react.useState)("");
|
|
@@ -372,37 +171,32 @@ window.__ModuleLoader__.load({
|
|
|
372
171
|
index: 0,
|
|
373
172
|
elapsed: 0
|
|
374
173
|
});
|
|
174
|
+
const cell = definition.cell;
|
|
175
|
+
const columns = definition.columns;
|
|
176
|
+
const rows = definition.rows;
|
|
177
|
+
const tracks = definition.tracks;
|
|
375
178
|
(0, react.useEffect)(() => {
|
|
376
179
|
let cancelled = false;
|
|
377
180
|
const img = new Image();
|
|
378
181
|
img.onload = () => {
|
|
379
|
-
if (cancelled)
|
|
380
|
-
setImageReady(true);
|
|
381
|
-
fetch(PET_MANIFEST_URL).then((res) => res.ok ? res.json() : Promise.resolve({})).then((manifest) => {
|
|
382
|
-
if (cancelled) return;
|
|
383
|
-
const frames = manifest.frames;
|
|
384
|
-
if (Array.isArray(frames) && frames.length === 9 && frames.every((n) => typeof n === "number")) setFrameCounts(frames);
|
|
385
|
-
else setFrameCounts(detectFrameCounts(img));
|
|
386
|
-
}).catch(() => {
|
|
387
|
-
if (!cancelled) setFrameCounts(detectFrameCounts(img));
|
|
388
|
-
});
|
|
182
|
+
if (!cancelled) setImageReady(true);
|
|
389
183
|
};
|
|
390
|
-
img.src =
|
|
184
|
+
img.src = definition.atlasUrl;
|
|
391
185
|
return () => {
|
|
392
186
|
cancelled = true;
|
|
393
187
|
img.onload = null;
|
|
394
188
|
};
|
|
395
|
-
}, []);
|
|
396
|
-
const spriteScale = display.size /
|
|
189
|
+
}, [definition.atlasUrl]);
|
|
190
|
+
const spriteScale = display.size / cell.height;
|
|
397
191
|
const animation = snapshot?.animation ?? "idle";
|
|
398
192
|
const scaleRef = (0, react.useRef)(spriteScale);
|
|
399
193
|
scaleRef.current = spriteScale;
|
|
400
194
|
(0, react.useEffect)(() => {
|
|
401
195
|
const reduceMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches === true;
|
|
402
196
|
const row = rowOfTrack(animation);
|
|
403
|
-
const leadCol =
|
|
404
|
-
const lead = framePosition(row, leadCol, scaleRef.current);
|
|
405
|
-
if (spriteRef.current !== null) spriteRef.current.style.backgroundPosition =
|
|
197
|
+
const leadCol = trimTrack(tracks[animation], rows[row] ?? tracks[animation].frames.length).frames[0];
|
|
198
|
+
const lead = framePosition(cell, columns, row, leadCol, scaleRef.current);
|
|
199
|
+
if (spriteRef.current !== null) spriteRef.current.style.backgroundPosition = lead.x + "px " + lead.y + "px";
|
|
406
200
|
if (reduceMotion) return;
|
|
407
201
|
let raf = 0;
|
|
408
202
|
let last = performance.now();
|
|
@@ -410,7 +204,7 @@ window.__ModuleLoader__.load({
|
|
|
410
204
|
const delta = ts - last;
|
|
411
205
|
last = ts;
|
|
412
206
|
const row = rowOfTrack(animation);
|
|
413
|
-
const track =
|
|
207
|
+
const track = trimTrack(tracks[animation], rows[row] ?? tracks[animation].frames.length);
|
|
414
208
|
const st = frameRef.current;
|
|
415
209
|
if (st.track !== animation) {
|
|
416
210
|
st.track = animation;
|
|
@@ -428,13 +222,19 @@ window.__ModuleLoader__.load({
|
|
|
428
222
|
st.index = 0;
|
|
429
223
|
} else st.index = maxIndex;
|
|
430
224
|
const col = track.frames[st.index];
|
|
431
|
-
const
|
|
432
|
-
if (spriteRef.current !== null) spriteRef.current.style.backgroundPosition =
|
|
225
|
+
const pos = framePosition(cell, columns, row, col, scaleRef.current);
|
|
226
|
+
if (spriteRef.current !== null) spriteRef.current.style.backgroundPosition = pos.x + "px " + pos.y + "px";
|
|
433
227
|
raf = requestAnimationFrame(tick);
|
|
434
228
|
};
|
|
435
229
|
raf = requestAnimationFrame(tick);
|
|
436
230
|
return () => cancelAnimationFrame(raf);
|
|
437
|
-
}, [
|
|
231
|
+
}, [
|
|
232
|
+
animation,
|
|
233
|
+
cell,
|
|
234
|
+
columns,
|
|
235
|
+
rows,
|
|
236
|
+
tracks
|
|
237
|
+
]);
|
|
438
238
|
const feedbackDoneRef = (0, react.useRef)(props.onFeedbackDone);
|
|
439
239
|
feedbackDoneRef.current = props.onFeedbackDone;
|
|
440
240
|
(0, react.useEffect)(() => {
|
|
@@ -486,9 +286,10 @@ window.__ModuleLoader__.load({
|
|
|
486
286
|
right: display.right,
|
|
487
287
|
bottom: display.bottom
|
|
488
288
|
};
|
|
489
|
-
const spriteWidth = Math.round(
|
|
490
|
-
const spriteHeight = Math.round(
|
|
289
|
+
const spriteWidth = Math.round(cell.width * spriteScale);
|
|
290
|
+
const spriteHeight = Math.round(cell.height * spriteScale);
|
|
491
291
|
const statusBubble = feedback === null && !hovered ? snapshot?.bubble : void 0;
|
|
292
|
+
const displayName = snapshot?.name ?? definition.displayName;
|
|
492
293
|
return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
493
294
|
ref: floatRef,
|
|
494
295
|
className: pet_module_css_default.float,
|
|
@@ -514,8 +315,8 @@ window.__ModuleLoader__.load({
|
|
|
514
315
|
style: {
|
|
515
316
|
width: spriteWidth,
|
|
516
317
|
height: spriteHeight,
|
|
517
|
-
backgroundImage: imageReady ?
|
|
518
|
-
backgroundSize:
|
|
318
|
+
backgroundImage: imageReady ? "url(" + definition.atlasUrl + ")" : void 0,
|
|
319
|
+
backgroundSize: cell.width * columns * spriteScale + "px " + cell.height * rows.length * spriteScale + "px",
|
|
519
320
|
backgroundRepeat: "no-repeat",
|
|
520
321
|
backgroundPosition: "0 0",
|
|
521
322
|
cursor: dragRef.current === null ? "grab" : "grabbing"
|
|
@@ -528,14 +329,14 @@ window.__ModuleLoader__.load({
|
|
|
528
329
|
props.onPet();
|
|
529
330
|
},
|
|
530
331
|
role: "button",
|
|
531
|
-
"aria-label":
|
|
332
|
+
"aria-label": definition.displayName
|
|
532
333
|
}),
|
|
533
334
|
feedback !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
534
|
-
className:
|
|
335
|
+
className: clsx(pet_module_css_default.bubble, feedback.kind === "feed" ? pet_module_css_default.bubbleFeed : pet_module_css_default.bubblePet),
|
|
535
336
|
children: feedback.text
|
|
536
337
|
}, feedback.at),
|
|
537
338
|
statusBubble !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
538
|
-
className:
|
|
339
|
+
className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus),
|
|
539
340
|
role: "status",
|
|
540
341
|
"aria-live": "polite",
|
|
541
342
|
children: statusBubble
|
|
@@ -581,7 +382,7 @@ window.__ModuleLoader__.load({
|
|
|
581
382
|
className: pet_module_css_default.rankRow,
|
|
582
383
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
583
384
|
className: pet_module_css_default.nameCell,
|
|
584
|
-
children:
|
|
385
|
+
children: displayName
|
|
585
386
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: props.t("pet.rank", { rank: snapshot?.affinity.rank ?? "?" }) })]
|
|
586
387
|
}),
|
|
587
388
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -601,7 +402,7 @@ window.__ModuleLoader__.load({
|
|
|
601
402
|
type: "button",
|
|
602
403
|
className: pet_module_css_default.action,
|
|
603
404
|
onClick: () => {
|
|
604
|
-
setNameDraft(
|
|
405
|
+
setNameDraft(displayName);
|
|
605
406
|
setRenaming(true);
|
|
606
407
|
},
|
|
607
408
|
children: props.t("pet.rename")
|
|
@@ -623,12 +424,14 @@ window.__ModuleLoader__.load({
|
|
|
623
424
|
//#region src/client/PetDockEntry.tsx
|
|
624
425
|
/**
|
|
625
426
|
* Global floating pet entry. The pet is host-global (its state, display and
|
|
626
|
-
* interactions live on
|
|
427
|
+
* interactions live on '/api/pet/*' endpoints with no session dimension), so
|
|
627
428
|
* it must not ride a session-scoped slot — on the new-conversation screen no
|
|
628
429
|
* session exists to scope a slot by, and the pet would vanish (issue #48).
|
|
629
|
-
* The client half therefore mounts this entry straight onto
|
|
630
|
-
* (see index.ts): while visible it renders the floating
|
|
631
|
-
* while hidden it renders a fixed-position summon button.
|
|
430
|
+
* The client half therefore mounts this entry straight onto 'document.body'
|
|
431
|
+
* (see index.ts): while visible it renders the floating PetSprite (a
|
|
432
|
+
* portal), while hidden it renders a fixed-position summon button. Which
|
|
433
|
+
* sprite renders is decided by the host snapshot's pet id resolved against
|
|
434
|
+
* the registry list — no per-pet component exists.
|
|
632
435
|
* @module @linxin666/dsh-pet/client/PetDockEntry
|
|
633
436
|
*/
|
|
634
437
|
const DEFAULT_DISPLAY = {
|
|
@@ -638,7 +441,7 @@ window.__ModuleLoader__.load({
|
|
|
638
441
|
bottom: 20
|
|
639
442
|
};
|
|
640
443
|
/**
|
|
641
|
-
* Dock entry: while the pet is visible, mount the floating
|
|
444
|
+
* Dock entry: while the pet is visible, mount the floating PetSprite (it
|
|
642
445
|
* portals itself onto document.body); while hidden, render the summon
|
|
643
446
|
* button so the pet can always come back. The store is the plugin-owned
|
|
644
447
|
* single instance — the slot system provides none because the pet is
|
|
@@ -649,6 +452,7 @@ window.__ModuleLoader__.load({
|
|
|
649
452
|
const ui = (0, react.useSyncExternalStore)(store.subscribe, store.getSnapshot);
|
|
650
453
|
const snapshot = ui.snapshot;
|
|
651
454
|
const feedback = ui.feedback;
|
|
455
|
+
const definition = ui.pets.find((entry) => entry.id === snapshot?.pet.id) ?? null;
|
|
652
456
|
const visible = snapshot?.display.visible ?? true;
|
|
653
457
|
(0, react.useEffect)(() => {
|
|
654
458
|
ensure();
|
|
@@ -656,9 +460,10 @@ window.__ModuleLoader__.load({
|
|
|
656
460
|
if (visible) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
657
461
|
"data-pet-dock": true,
|
|
658
462
|
"data-testid": "pet-dock",
|
|
659
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
463
|
+
children: snapshot === null || definition === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PetSprite, {
|
|
660
464
|
snapshot,
|
|
661
|
-
|
|
465
|
+
definition,
|
|
466
|
+
display: snapshot.display,
|
|
662
467
|
feedback,
|
|
663
468
|
onPet: props.pet,
|
|
664
469
|
onFeed: props.feed,
|
|
@@ -681,12 +486,12 @@ window.__ModuleLoader__.load({
|
|
|
681
486
|
},
|
|
682
487
|
onClick: props.summon,
|
|
683
488
|
"data-testid": "pet-summon",
|
|
684
|
-
children: props.t("pet.summon", { name: snapshot?.name ?? "
|
|
489
|
+
children: props.t("pet.summon", { name: snapshot?.name ?? "" })
|
|
685
490
|
});
|
|
686
491
|
}
|
|
687
492
|
//#endregion
|
|
688
493
|
//#region \0dsh-css:packages/dsh-pet/src/client/settings-card.module.css.mjs
|
|
689
|
-
const css = ".kKk9aW_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.kKk9aW_card:hover{border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.kKk9aW_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.kKk9aW_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.kKk9aW_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.kKk9aW_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.kKk9aW_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.kKk9aW_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.kKk9aW_chevronOpen{transform:rotate(180deg)}.kKk9aW_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.kKk9aW_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.kKk9aW_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.kKk9aW_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.kKk9aW_failed{min-width:0;color:var(--dsw-alias-label-error);flex:1;margin:0;font-size:12px;line-height:1.5}.kKk9aW_discard,.kKk9aW_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.kKk9aW_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.kKk9aW_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.kKk9aW_discard:disabled,.kKk9aW_save:disabled{opacity:.4;cursor:default}.kKk9aW_discard:focus-visible,.kKk9aW_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.kKk9aW_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.kKk9aW_field+.kKk9aW_field{border-top:1px solid var(--dsw-alias-border-l2)}.kKk9aW_head{align-items:center;gap:8px;display:flex}.kKk9aW_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.kKk9aW_badges{align-items:center;gap:8px;display:inline-flex}.kKk9aW_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.kKk9aW_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.kKk9aW_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.kKk9aW_reset:disabled{cursor:default}.kKk9aW_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.kKk9aW_input,.kKk9aW_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.kKk9aW_input:focus-visible,.kKk9aW_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.kKk9aW_input:disabled,.kKk9aW_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.kKk9aW_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.kKk9aW_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-label-error);outline-offset:1px;border-color:var(--dsw-alias-label-error)}.kKk9aW_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.kKk9aW_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.kKk9aW_card,.kKk9aW_header,.kKk9aW_chevron,.kKk9aW_chevronOpen,.kKk9aW_discard,.kKk9aW_save{transition:none}}";
|
|
494
|
+
const css = ".kKk9aW_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.kKk9aW_card:hover{border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.kKk9aW_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.kKk9aW_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.kKk9aW_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.kKk9aW_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.kKk9aW_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.kKk9aW_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.kKk9aW_chevronOpen{transform:rotate(180deg)}.kKk9aW_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.kKk9aW_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.kKk9aW_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.kKk9aW_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.kKk9aW_failed{min-width:0;color:var(--dsw-alias-label-error);text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:12px;line-height:1.5;overflow:hidden}.kKk9aW_discard,.kKk9aW_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.kKk9aW_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.kKk9aW_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.kKk9aW_discard:disabled,.kKk9aW_save:disabled{opacity:.4;cursor:default}.kKk9aW_discard:focus-visible,.kKk9aW_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.kKk9aW_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.kKk9aW_field+.kKk9aW_field{border-top:1px solid var(--dsw-alias-border-l2)}.kKk9aW_head{align-items:center;gap:8px;display:flex}.kKk9aW_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.kKk9aW_badges{align-items:center;gap:8px;display:inline-flex}.kKk9aW_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.kKk9aW_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.kKk9aW_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.kKk9aW_reset:disabled{cursor:default}.kKk9aW_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.kKk9aW_input,.kKk9aW_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.kKk9aW_input:focus-visible,.kKk9aW_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.kKk9aW_input:disabled,.kKk9aW_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.kKk9aW_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.kKk9aW_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-label-error);outline-offset:1px;border-color:var(--dsw-alias-label-error)}.kKk9aW_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.kKk9aW_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.kKk9aW_card,.kKk9aW_header,.kKk9aW_chevron,.kKk9aW_chevronOpen,.kKk9aW_discard,.kKk9aW_save{transition:none}}";
|
|
690
495
|
const tagId = "@linxin666/dsh-pet/settings-card.module.css";
|
|
691
496
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
692
497
|
const tag = document.createElement("style");
|
|
@@ -727,12 +532,12 @@ window.__ModuleLoader__.load({
|
|
|
727
532
|
//#endregion
|
|
728
533
|
//#region src/client/PluginSettingsCard.tsx
|
|
729
534
|
/**
|
|
730
|
-
*
|
|
731
|
-
* plugin and what its settings govern, the controls inside, and the save
|
|
732
|
-
* writes them. Renders nothing while the namespace is unavailable — a
|
|
535
|
+
* Family-shared chrome for plugin settings cards: a disclosure header naming
|
|
536
|
+
* the plugin and what its settings govern, the controls inside, and the save
|
|
537
|
+
* that writes them. Renders nothing while the namespace is unavailable — a
|
|
733
538
|
* deployment that does not compose the owning plugin should show no trace of
|
|
734
|
-
* it.
|
|
735
|
-
*
|
|
539
|
+
* it. Inlined into each consumer's client bundle; mirrors the official
|
|
540
|
+
* ui-plugin-config PluginCard in a self-contained slice.
|
|
736
541
|
*/
|
|
737
542
|
/**
|
|
738
543
|
* Render one plugin settings card.
|
|
@@ -744,6 +549,7 @@ window.__ModuleLoader__.load({
|
|
|
744
549
|
const { state } = props;
|
|
745
550
|
if (!state.available) return null;
|
|
746
551
|
const title = props.t(props.titleKey);
|
|
552
|
+
const description = props.t(props.descriptionKey);
|
|
747
553
|
const blocked = !state.dirty || state.invalid || state.saving;
|
|
748
554
|
const cardClass = open ? `${settings_card_module_css_default.cardOpen} ${settings_card_module_css_default.card}` : settings_card_module_css_default.card;
|
|
749
555
|
if (!state.exposed) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
@@ -760,10 +566,12 @@ window.__ModuleLoader__.load({
|
|
|
760
566
|
className: settings_card_module_css_default.headText,
|
|
761
567
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
762
568
|
className: settings_card_module_css_default.name,
|
|
569
|
+
title,
|
|
763
570
|
children: title
|
|
764
571
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
765
572
|
className: settings_card_module_css_default.description,
|
|
766
|
-
|
|
573
|
+
title: description,
|
|
574
|
+
children: description
|
|
767
575
|
})]
|
|
768
576
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
769
577
|
width: "14",
|
|
@@ -801,14 +609,17 @@ window.__ModuleLoader__.load({
|
|
|
801
609
|
className: settings_card_module_css_default.headText,
|
|
802
610
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
803
611
|
className: settings_card_module_css_default.name,
|
|
612
|
+
title,
|
|
804
613
|
children: title
|
|
805
614
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
806
615
|
className: settings_card_module_css_default.description,
|
|
807
|
-
|
|
616
|
+
title: description,
|
|
617
|
+
children: description
|
|
808
618
|
})]
|
|
809
619
|
}),
|
|
810
620
|
state.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
811
621
|
className: settings_card_module_css_default.pending,
|
|
622
|
+
title: props.t("settings.unsaved"),
|
|
812
623
|
children: props.t("settings.unsaved")
|
|
813
624
|
}) : null,
|
|
814
625
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
@@ -836,10 +647,10 @@ window.__ModuleLoader__.load({
|
|
|
836
647
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
837
648
|
className: settings_card_module_css_default.footer,
|
|
838
649
|
children: [
|
|
839
|
-
state.failed ? /* @__PURE__ */ (0, react_jsx_runtime.
|
|
650
|
+
state.failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
840
651
|
className: settings_card_module_css_default.failed,
|
|
841
652
|
role: "status",
|
|
842
|
-
children: props.t("settings.saveFailed")
|
|
653
|
+
children: [props.t("settings.saveFailed"), state.failedReason ? " - " + state.failedReason : ""]
|
|
843
654
|
}) : null,
|
|
844
655
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
845
656
|
type: "button",
|
|
@@ -961,10 +772,59 @@ window.__ModuleLoader__.load({
|
|
|
961
772
|
]
|
|
962
773
|
});
|
|
963
774
|
}
|
|
775
|
+
/** A staged enumerated field rendered as a select. */
|
|
776
|
+
function ChoiceField(props) {
|
|
777
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
778
|
+
className: settings_card_module_css_default.field,
|
|
779
|
+
children: [
|
|
780
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
781
|
+
className: settings_card_module_css_default.head,
|
|
782
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
783
|
+
className: settings_card_module_css_default.label,
|
|
784
|
+
htmlFor: props.id,
|
|
785
|
+
children: props.label
|
|
786
|
+
}), props.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
787
|
+
className: settings_card_module_css_default.badges,
|
|
788
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
789
|
+
className: settings_card_module_css_default.badge,
|
|
790
|
+
children: props.overriddenLabel
|
|
791
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
792
|
+
type: "button",
|
|
793
|
+
className: settings_card_module_css_default.reset,
|
|
794
|
+
disabled: props.disabled,
|
|
795
|
+
onClick: props.onReset,
|
|
796
|
+
children: props.resetLabel
|
|
797
|
+
})]
|
|
798
|
+
}) : null]
|
|
799
|
+
}),
|
|
800
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
801
|
+
id: props.id,
|
|
802
|
+
className: settings_card_module_css_default.select,
|
|
803
|
+
value: props.text,
|
|
804
|
+
disabled: props.disabled,
|
|
805
|
+
onChange: (event) => {
|
|
806
|
+
props.onEdit(event.target.value);
|
|
807
|
+
},
|
|
808
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
809
|
+
value: "",
|
|
810
|
+
children: props.inheritLabel
|
|
811
|
+
}), props.choices.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
812
|
+
value: choice.value,
|
|
813
|
+
children: choice.label
|
|
814
|
+
}, choice.value))]
|
|
815
|
+
}),
|
|
816
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
817
|
+
className: props.invalid ? settings_card_module_css_default.invalid : settings_card_module_css_default.hint,
|
|
818
|
+
children: props.invalid ? props.invalidLabel : props.hint
|
|
819
|
+
})
|
|
820
|
+
]
|
|
821
|
+
});
|
|
822
|
+
}
|
|
964
823
|
//#endregion
|
|
965
824
|
//#region src/client/settings-form.ts
|
|
966
|
-
/** A whole-number field. An empty draft clears the field; any other draft that is not a finite number blocks the save. */
|
|
967
|
-
function numberField(field) {
|
|
825
|
+
/** A whole- or decimal-number field. An empty draft clears the field; any other draft that is not a finite number within the constraints blocks the save. */
|
|
826
|
+
function numberField(field, constraints = {}) {
|
|
827
|
+
const { integer = false, min } = constraints;
|
|
968
828
|
return {
|
|
969
829
|
field,
|
|
970
830
|
format: (value) => typeof value === "number" ? String(value) : "",
|
|
@@ -972,23 +832,12 @@ window.__ModuleLoader__.load({
|
|
|
972
832
|
const trimmed = text.trim();
|
|
973
833
|
if (trimmed === "") return { kind: "clear" };
|
|
974
834
|
const parsed = Number(trimmed);
|
|
975
|
-
|
|
835
|
+
if (!Number.isFinite(parsed)) return void 0;
|
|
836
|
+
if (integer && !Number.isInteger(parsed)) return void 0;
|
|
837
|
+
if (min !== void 0 && parsed < min) return void 0;
|
|
838
|
+
return {
|
|
976
839
|
kind: "set",
|
|
977
840
|
value: parsed
|
|
978
|
-
} : void 0;
|
|
979
|
-
}
|
|
980
|
-
};
|
|
981
|
-
}
|
|
982
|
-
/** A free-text field. An empty draft clears the field. */
|
|
983
|
-
function textField(field) {
|
|
984
|
-
return {
|
|
985
|
-
field,
|
|
986
|
-
format: (value) => typeof value === "string" ? value : "",
|
|
987
|
-
parse: (text) => {
|
|
988
|
-
const trimmed = text.trim();
|
|
989
|
-
return trimmed === "" ? { kind: "clear" } : {
|
|
990
|
-
kind: "set",
|
|
991
|
-
value: trimmed
|
|
992
841
|
};
|
|
993
842
|
}
|
|
994
843
|
};
|
|
@@ -999,17 +848,33 @@ window.__ModuleLoader__.load({
|
|
|
999
848
|
field,
|
|
1000
849
|
format: (value) => typeof value === "boolean" ? String(value) : "",
|
|
1001
850
|
parse: (text) => {
|
|
1002
|
-
|
|
851
|
+
const trimmed = text.trim();
|
|
852
|
+
if (trimmed === "") return { kind: "clear" };
|
|
853
|
+
if (trimmed === "true") return {
|
|
1003
854
|
kind: "set",
|
|
1004
855
|
value: true
|
|
1005
856
|
};
|
|
1006
|
-
if (
|
|
857
|
+
if (trimmed === "false") return {
|
|
1007
858
|
kind: "set",
|
|
1008
859
|
value: false
|
|
1009
860
|
};
|
|
1010
861
|
}
|
|
1011
862
|
};
|
|
1012
863
|
}
|
|
864
|
+
/** An enumerated string field; only the listed choices are accepted. An empty draft clears the field. */
|
|
865
|
+
function choiceField(field, choices) {
|
|
866
|
+
return {
|
|
867
|
+
field,
|
|
868
|
+
format: (value) => typeof value === "string" && choices.includes(value) ? value : "",
|
|
869
|
+
parse: (text) => {
|
|
870
|
+
if (text === "") return { kind: "clear" };
|
|
871
|
+
return choices.includes(text) ? {
|
|
872
|
+
kind: "set",
|
|
873
|
+
value: text
|
|
874
|
+
} : void 0;
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
}
|
|
1013
878
|
/**
|
|
1014
879
|
* Stages one card's edits over one settings namespace and writes them on save.
|
|
1015
880
|
*
|
|
@@ -1025,6 +890,7 @@ window.__ModuleLoader__.load({
|
|
|
1025
890
|
listeners = /* @__PURE__ */ new Set();
|
|
1026
891
|
saving = false;
|
|
1027
892
|
failed = false;
|
|
893
|
+
failedReason;
|
|
1028
894
|
/** @param scope - the bound settings scope for this card's namespace. */
|
|
1029
895
|
constructor(scope, specs) {
|
|
1030
896
|
this.scope = scope;
|
|
@@ -1052,7 +918,8 @@ window.__ModuleLoader__.load({
|
|
|
1052
918
|
dirty: plan.length > 0,
|
|
1053
919
|
invalid: plan.some((item) => item.run === void 0),
|
|
1054
920
|
saving: this.saving,
|
|
1055
|
-
failed: this.failed
|
|
921
|
+
failed: this.failed,
|
|
922
|
+
...this.failedReason === void 0 ? {} : { failedReason: this.failedReason }
|
|
1056
923
|
};
|
|
1057
924
|
}
|
|
1058
925
|
/** Read one field's state from the effective section and its staged draft. */
|
|
@@ -1093,28 +960,50 @@ window.__ModuleLoader__.load({
|
|
|
1093
960
|
if (this.staged.size === 0 && !this.failed) return;
|
|
1094
961
|
this.staged.clear();
|
|
1095
962
|
this.failed = false;
|
|
963
|
+
this.failedReason = void 0;
|
|
1096
964
|
this.publish();
|
|
1097
965
|
}
|
|
1098
966
|
};
|
|
1099
967
|
}
|
|
1100
968
|
/**
|
|
1101
969
|
* Write every staged edit, then re-seed from what the Host accepted.
|
|
970
|
+
*
|
|
971
|
+
* When the scope carries the optional batch surface (the dsh-web-ui
|
|
972
|
+
* bridge scope), every planned write rides one mutation so cross-field
|
|
973
|
+
* validate hooks (baseURL+model) judge the batch as a unit instead of
|
|
974
|
+
* deadlocking on per-field writes. Otherwise the per-field loop runs.
|
|
975
|
+
* A field lands only when the Host reports it held the staged value; a
|
|
976
|
+
* landed field's draft is dropped, a failed one stays staged for the user.
|
|
1102
977
|
* @returns settlement after every write and the read-back.
|
|
1103
978
|
*/
|
|
1104
979
|
async save() {
|
|
1105
980
|
const plan = this.plan();
|
|
1106
|
-
const
|
|
1107
|
-
if (plan.length === 0 || this.saving ||
|
|
981
|
+
const valid = plan.filter((item) => item.run !== void 0);
|
|
982
|
+
if (plan.length === 0 || this.saving || valid.length !== plan.length) return;
|
|
983
|
+
const plannedWrites = valid.map((item) => item.op);
|
|
984
|
+
const fields = new Set(plan.map((item) => item.field));
|
|
1108
985
|
this.saving = true;
|
|
1109
986
|
this.failed = false;
|
|
987
|
+
this.failedReason = void 0;
|
|
1110
988
|
this.publish();
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
if (
|
|
989
|
+
const landed = /* @__PURE__ */ new Set();
|
|
990
|
+
const batch = this.batchedScope();
|
|
991
|
+
if (batch !== void 0) {
|
|
992
|
+
const result = await batch.mutate(plannedWrites);
|
|
993
|
+
if (result.ok) {
|
|
994
|
+
for (const field of result.fields) if (field.landed) landed.add(field.field);
|
|
995
|
+
} else this.failedReason = result.message;
|
|
996
|
+
} else for (const item of valid) if (await item.run()) landed.add(item.field);
|
|
997
|
+
for (const field of fields) if (landed.has(field)) this.staged.delete(field);
|
|
1114
998
|
this.saving = false;
|
|
1115
|
-
this.failed =
|
|
999
|
+
this.failed = landed.size !== fields.size;
|
|
1116
1000
|
this.publish();
|
|
1117
1001
|
}
|
|
1002
|
+
/** The scope's batch surface when it supports one; undefined conservatively otherwise. */
|
|
1003
|
+
batchedScope() {
|
|
1004
|
+
const candidate = this.scope;
|
|
1005
|
+
return typeof candidate?.mutate === "function" ? candidate : void 0;
|
|
1006
|
+
}
|
|
1118
1007
|
/**
|
|
1119
1008
|
* Every staged edit a save would write. An entry whose draft is not a value
|
|
1120
1009
|
* its field accepts carries no write: the form is still dirty, and the save
|
|
@@ -1129,6 +1018,10 @@ window.__ModuleLoader__.load({
|
|
|
1129
1018
|
if (staged.clear) {
|
|
1130
1019
|
if (this.stored(field)) plan.push({
|
|
1131
1020
|
field,
|
|
1021
|
+
op: {
|
|
1022
|
+
field,
|
|
1023
|
+
op: "unset"
|
|
1024
|
+
},
|
|
1132
1025
|
run: () => this.clear(field)
|
|
1133
1026
|
});
|
|
1134
1027
|
continue;
|
|
@@ -1137,14 +1030,27 @@ window.__ModuleLoader__.load({
|
|
|
1137
1030
|
const write = spec.parse(staged.text);
|
|
1138
1031
|
if (write === void 0) plan.push({
|
|
1139
1032
|
field,
|
|
1033
|
+
op: {
|
|
1034
|
+
field,
|
|
1035
|
+
op: "unset"
|
|
1036
|
+
},
|
|
1140
1037
|
run: void 0
|
|
1141
1038
|
});
|
|
1142
1039
|
else if (write.kind === "clear") plan.push({
|
|
1143
1040
|
field,
|
|
1041
|
+
op: {
|
|
1042
|
+
field,
|
|
1043
|
+
op: "unset"
|
|
1044
|
+
},
|
|
1144
1045
|
run: () => this.clear(field)
|
|
1145
1046
|
});
|
|
1146
1047
|
else plan.push({
|
|
1147
1048
|
field,
|
|
1049
|
+
op: {
|
|
1050
|
+
field,
|
|
1051
|
+
op: "set",
|
|
1052
|
+
value: write.value
|
|
1053
|
+
},
|
|
1148
1054
|
run: () => this.store(field, write.value)
|
|
1149
1055
|
});
|
|
1150
1056
|
}
|
|
@@ -1156,11 +1062,13 @@ window.__ModuleLoader__.load({
|
|
|
1156
1062
|
}
|
|
1157
1063
|
async store(field, value) {
|
|
1158
1064
|
await this.scope.set(field, value);
|
|
1065
|
+
if (this.specOf(field).secret) return true;
|
|
1159
1066
|
return this.userLayer()?.[field] === value;
|
|
1160
1067
|
}
|
|
1161
1068
|
stage(field, edit) {
|
|
1162
1069
|
this.staged.set(field, edit);
|
|
1163
1070
|
this.failed = false;
|
|
1071
|
+
this.failedReason = void 0;
|
|
1164
1072
|
this.publish();
|
|
1165
1073
|
}
|
|
1166
1074
|
specOf(field) {
|
|
@@ -1190,11 +1098,21 @@ window.__ModuleLoader__.load({
|
|
|
1190
1098
|
};
|
|
1191
1099
|
//#endregion
|
|
1192
1100
|
//#region src/client/PetSettingsCard.tsx
|
|
1193
|
-
/**
|
|
1101
|
+
/** Fetch the registry list (the same data the sprite renders from). */
|
|
1102
|
+
async function fetchPetChoices() {
|
|
1103
|
+
const response = await fetch("/api/pet/pets");
|
|
1104
|
+
if (!response.ok) throw new Error("pet pets failed: " + response.status);
|
|
1105
|
+
return await response.json();
|
|
1106
|
+
}
|
|
1107
|
+
/** Bridges the 'pet' scope onto the card's staged form. */
|
|
1194
1108
|
var PetSettingsCardController = class {
|
|
1195
1109
|
form;
|
|
1196
1110
|
store;
|
|
1197
|
-
|
|
1111
|
+
petChoices = [];
|
|
1112
|
+
petLabels = /* @__PURE__ */ new Map();
|
|
1113
|
+
loaded = false;
|
|
1114
|
+
attempts = 0;
|
|
1115
|
+
/** @param scope - the bound settings scope for the 'pet' namespace. */
|
|
1198
1116
|
constructor(scope) {
|
|
1199
1117
|
this.form = new CardForm(scope, [
|
|
1200
1118
|
booleanField("enabled"),
|
|
@@ -1202,9 +1120,26 @@ window.__ModuleLoader__.load({
|
|
|
1202
1120
|
numberField("size"),
|
|
1203
1121
|
numberField("right"),
|
|
1204
1122
|
numberField("bottom"),
|
|
1205
|
-
|
|
1123
|
+
choiceField("petId", this.petChoices)
|
|
1206
1124
|
]);
|
|
1207
1125
|
this.store = this.form.bind(() => this.projection());
|
|
1126
|
+
this.loadPets();
|
|
1127
|
+
}
|
|
1128
|
+
/** Resolve the registry choices once (retried a few times on failure). */
|
|
1129
|
+
async loadPets() {
|
|
1130
|
+
if (this.loaded) return;
|
|
1131
|
+
try {
|
|
1132
|
+
const list = await fetchPetChoices();
|
|
1133
|
+
this.petChoices.splice(0, this.petChoices.length, ...list.map((choice) => choice.id));
|
|
1134
|
+
for (const choice of list) this.petLabels.set(choice.id, choice.displayName);
|
|
1135
|
+
this.loaded = true;
|
|
1136
|
+
this.store.set(this.projection());
|
|
1137
|
+
} catch {
|
|
1138
|
+
this.attempts += 1;
|
|
1139
|
+
if (this.attempts < 3) window.setTimeout(() => {
|
|
1140
|
+
this.loadPets();
|
|
1141
|
+
}, 3e3);
|
|
1142
|
+
}
|
|
1208
1143
|
}
|
|
1209
1144
|
projection() {
|
|
1210
1145
|
return {
|
|
@@ -1214,7 +1149,11 @@ window.__ModuleLoader__.load({
|
|
|
1214
1149
|
size: this.form.field("size"),
|
|
1215
1150
|
right: this.form.field("right"),
|
|
1216
1151
|
bottom: this.form.field("bottom"),
|
|
1217
|
-
|
|
1152
|
+
petId: this.form.field("petId"),
|
|
1153
|
+
petChoices: this.petChoices.map((id) => ({
|
|
1154
|
+
value: id,
|
|
1155
|
+
label: this.petLabels.get(id) ?? id
|
|
1156
|
+
}))
|
|
1218
1157
|
};
|
|
1219
1158
|
}
|
|
1220
1159
|
/**
|
|
@@ -1267,6 +1206,21 @@ window.__ModuleLoader__.load({
|
|
|
1267
1206
|
props.resetField("enabled");
|
|
1268
1207
|
}
|
|
1269
1208
|
}),
|
|
1209
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChoiceField, {
|
|
1210
|
+
id: "settings-pet-pet",
|
|
1211
|
+
label: t("settings.pet"),
|
|
1212
|
+
hint: t("settings.petHint"),
|
|
1213
|
+
inheritLabel: t("settings.inherit"),
|
|
1214
|
+
...fieldProps,
|
|
1215
|
+
...state.petId,
|
|
1216
|
+
choices: state.petChoices,
|
|
1217
|
+
onEdit: (text) => {
|
|
1218
|
+
props.edit("petId", text);
|
|
1219
|
+
},
|
|
1220
|
+
onReset: () => {
|
|
1221
|
+
props.resetField("petId");
|
|
1222
|
+
}
|
|
1223
|
+
}),
|
|
1270
1224
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BooleanField, {
|
|
1271
1225
|
id: "settings-pet-visible",
|
|
1272
1226
|
label: t("settings.visible"),
|
|
@@ -1324,19 +1278,6 @@ window.__ModuleLoader__.load({
|
|
|
1324
1278
|
onReset: () => {
|
|
1325
1279
|
props.resetField("bottom");
|
|
1326
1280
|
}
|
|
1327
|
-
}),
|
|
1328
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ValueField, {
|
|
1329
|
-
id: "settings-pet-name",
|
|
1330
|
-
label: t("settings.name"),
|
|
1331
|
-
hint: t("settings.nameHint"),
|
|
1332
|
-
...fieldProps,
|
|
1333
|
-
...state.name,
|
|
1334
|
-
onEdit: (text) => {
|
|
1335
|
-
props.edit("name", text);
|
|
1336
|
-
},
|
|
1337
|
-
onReset: () => {
|
|
1338
|
-
props.resetField("name");
|
|
1339
|
-
}
|
|
1340
1281
|
})
|
|
1341
1282
|
]
|
|
1342
1283
|
});
|
|
@@ -1354,10 +1295,12 @@ window.__ModuleLoader__.load({
|
|
|
1354
1295
|
"pet.rank": "亲密度 {rank}",
|
|
1355
1296
|
"pet.points": "{points} 点",
|
|
1356
1297
|
"pet.treats": "小鱼干 ×{n}",
|
|
1357
|
-
"pet.state.loading": "
|
|
1358
|
-
"pet.state.error": "
|
|
1298
|
+
"pet.state.loading": "宠物正在赶来…",
|
|
1299
|
+
"pet.state.error": "宠物迷路了(连接失败)",
|
|
1359
1300
|
"settings.title": "宠物",
|
|
1360
|
-
"settings.description": "
|
|
1301
|
+
"settings.description": "选择宠物并调整它的显示布局。",
|
|
1302
|
+
"settings.pet": "宠物",
|
|
1303
|
+
"settings.petHint": "选择显示哪只宠物;每只宠物独立命名,可在宠物悬浮面板改名。",
|
|
1361
1304
|
"settings.enabled": "启用宠物",
|
|
1362
1305
|
"settings.enabledHint": "关闭后隐藏宠物并停止轮询,可在设置里重新启用。",
|
|
1363
1306
|
"settings.visible": "显示宠物",
|
|
@@ -1368,8 +1311,6 @@ window.__ModuleLoader__.load({
|
|
|
1368
1311
|
"settings.rightHint": "距视口右边缘的水平内缩距离。",
|
|
1369
1312
|
"settings.bottom": "距底部(px)",
|
|
1370
1313
|
"settings.bottomHint": "距视口底边的垂直内缩距离。",
|
|
1371
|
-
"settings.name": "名字",
|
|
1372
|
-
"settings.nameHint": "宠物显示名,1–20 个字符。",
|
|
1373
1314
|
"settings.inherit": "继承",
|
|
1374
1315
|
"settings.on": "开",
|
|
1375
1316
|
"settings.off": "关",
|
|
@@ -1397,10 +1338,12 @@ window.__ModuleLoader__.load({
|
|
|
1397
1338
|
"pet.rank": "Affinity {rank}",
|
|
1398
1339
|
"pet.points": "{points} pts",
|
|
1399
1340
|
"pet.treats": "Treats ×{n}",
|
|
1400
|
-
"pet.state.loading": "The
|
|
1401
|
-
"pet.state.error": "The
|
|
1341
|
+
"pet.state.loading": "The pet is on its way…",
|
|
1342
|
+
"pet.state.error": "The pet is lost (connection failed)",
|
|
1402
1343
|
"settings.title": "Pet",
|
|
1403
|
-
"settings.description": "
|
|
1344
|
+
"settings.description": "Pick a pet and tune its display layout.",
|
|
1345
|
+
"settings.pet": "Pet",
|
|
1346
|
+
"settings.petHint": "Choose which pet shows. Names are stored per pet; rename from the pet hover panel.",
|
|
1404
1347
|
"settings.enabled": "Enable the pet",
|
|
1405
1348
|
"settings.enabledHint": "When off, the pet hides and polling stops; re-enable it here.",
|
|
1406
1349
|
"settings.visible": "Show the pet",
|
|
@@ -1411,8 +1354,6 @@ window.__ModuleLoader__.load({
|
|
|
1411
1354
|
"settings.rightHint": "Horizontal inset from the viewport right edge.",
|
|
1412
1355
|
"settings.bottom": "Bottom inset (px)",
|
|
1413
1356
|
"settings.bottomHint": "Vertical inset from the viewport bottom edge.",
|
|
1414
|
-
"settings.name": "Name",
|
|
1415
|
-
"settings.nameHint": "The pet’s display name, 1–20 characters.",
|
|
1416
1357
|
"settings.inherit": "Inherit",
|
|
1417
1358
|
"settings.on": "On",
|
|
1418
1359
|
"settings.off": "Off",
|
|
@@ -1460,19 +1401,21 @@ window.__ModuleLoader__.load({
|
|
|
1460
1401
|
headers: { "content-type": "application/json" },
|
|
1461
1402
|
body: JSON.stringify(body)
|
|
1462
1403
|
});
|
|
1463
|
-
if (!response.ok) throw new Error(
|
|
1404
|
+
if (!response.ok) throw new Error("pet " + path + " failed: " + response.status);
|
|
1464
1405
|
return await response.json();
|
|
1465
1406
|
}
|
|
1466
1407
|
/** The live host API instance (always defined; failures surface per call). */
|
|
1467
1408
|
const petApi = {
|
|
1468
1409
|
state: () => petFetch("/api/pet/state"),
|
|
1410
|
+
pets: () => petFetch("/api/pet/pets"),
|
|
1469
1411
|
interact: (kind) => petFetch("/api/pet/interact", { kind }),
|
|
1470
1412
|
setVisible: (visible) => petFetch("/api/pet/set-visible", { visible }),
|
|
1471
1413
|
setConfig: (patch) => petFetch("/api/pet/set-config", patch),
|
|
1472
|
-
setName: (name) => petFetch("/api/pet/set-name", { name })
|
|
1414
|
+
setName: (name) => petFetch("/api/pet/set-name", { name }),
|
|
1415
|
+
setPet: (petId) => petFetch("/api/pet/set-pet", { petId })
|
|
1473
1416
|
};
|
|
1474
1417
|
/** Poll interval for the host snapshot. */
|
|
1475
|
-
const POLL_MS =
|
|
1418
|
+
const POLL_MS = 2e3;
|
|
1476
1419
|
/** Settings namespace the pet settings card edits (the Host plugin registers it). */
|
|
1477
1420
|
const PET_SETTINGS_NS = "pet";
|
|
1478
1421
|
/** Required services. */
|
|
@@ -1512,9 +1455,15 @@ window.__ModuleLoader__.load({
|
|
|
1512
1455
|
if (enabled() && disposeUi === void 0) {
|
|
1513
1456
|
const petStore = createPetStore().create();
|
|
1514
1457
|
const setSnapshot = petStore.actions.setSnapshot;
|
|
1458
|
+
const setPets = petStore.actions.setPets;
|
|
1515
1459
|
const setState = petStore.actions.setState;
|
|
1516
1460
|
const setFeedback = petStore.actions.setFeedback;
|
|
1461
|
+
let petsLoaded = false;
|
|
1517
1462
|
const pollNow = () => {
|
|
1463
|
+
if (!petsLoaded) petApi.pets().then((list) => {
|
|
1464
|
+
petsLoaded = true;
|
|
1465
|
+
setPets(list);
|
|
1466
|
+
}, () => {});
|
|
1518
1467
|
petApi.state().then((snapshot) => {
|
|
1519
1468
|
setSnapshot(snapshot);
|
|
1520
1469
|
}, () => {
|