@linxin666/dsh-pet 0.4.2 → 0.4.4
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 +16 -28
- package/README.zh.md +16 -28
- package/lib/client.js +74 -65
- package/lib/client.js.map +1 -1
- package/lib/index.js +169 -21
- package/lib/types/client/PetSettingsCard.d.ts +16 -12
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +77 -49
- package/lib/types/client/locales.d.ts +0 -2
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +0 -2
- package/lib/types/index.d.ts +6 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +82 -10
- package/lib/types/mount-once.d.ts +38 -7
- package/lib/types/mount-once.d.ts.map +1 -1
- package/lib/types/mount-once.js +96 -12
- package/lib/types/service.d.ts +9 -0
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +11 -1
- package/package.json +1 -1
- package/src/client/PetSettingsCard.test.tsx +115 -4
- package/src/client/PetSettingsCard.tsx +76 -66
- package/src/client/locales.ts +0 -2
- package/src/client/pet-css.test.ts +8 -0
- package/src/client/settings-section.module.css +0 -23
- package/src/index.test.ts +39 -0
- package/src/index.ts +105 -11
- package/src/mount-once.ts +107 -13
- package/src/service.ts +11 -1
|
@@ -61,8 +61,6 @@ export interface PetSettingsCardState extends CardShell {
|
|
|
61
61
|
decorationEnabled: CardFieldState
|
|
62
62
|
/** Pet choices (registry ids + display names), loaded from the host. */
|
|
63
63
|
petChoices: readonly { value: string; label: string }[]
|
|
64
|
-
/** Registry diagnostics (v1 migration hints, invalid entries), host-served. */
|
|
65
|
-
petDiagnostics: readonly PetDiagnosticView[]
|
|
66
64
|
}
|
|
67
65
|
|
|
68
66
|
/** The registration-side face the card's slot entry injects. */
|
|
@@ -79,12 +77,6 @@ interface PetChoice {
|
|
|
79
77
|
displayName: string
|
|
80
78
|
}
|
|
81
79
|
|
|
82
|
-
/** One registry diagnostic as served by '/api/pet/diagnostics' (#623). */
|
|
83
|
-
export interface PetDiagnosticView {
|
|
84
|
-
level: 'error' | 'warning'
|
|
85
|
-
message: string
|
|
86
|
-
}
|
|
87
|
-
|
|
88
80
|
/** Fetch the registry list (the same data the sprite renders from). */
|
|
89
81
|
async function fetchPetChoices(): Promise<PetChoice[]> {
|
|
90
82
|
const response = await fetch('/api/pet/pets')
|
|
@@ -92,21 +84,22 @@ async function fetchPetChoices(): Promise<PetChoice[]> {
|
|
|
92
84
|
return (await response.json()) as PetChoice[]
|
|
93
85
|
}
|
|
94
86
|
|
|
95
|
-
/**
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
87
|
+
/** The persisted state the fallback mirrors: the selection and its visibility. */
|
|
88
|
+
interface PetState {
|
|
89
|
+
/** Selected registry id. */
|
|
90
|
+
petId: string
|
|
91
|
+
/** Whether the pet is currently rendered. */
|
|
92
|
+
visible: boolean
|
|
101
93
|
}
|
|
102
94
|
|
|
103
|
-
/** Read the selection from the same persisted state the pet renders. */
|
|
104
|
-
async function
|
|
95
|
+
/** Read the selection and visibility from the same persisted state the pet renders. */
|
|
96
|
+
async function fetchPetState(): Promise<PetState> {
|
|
105
97
|
const response = await fetch('/api/pet/state')
|
|
106
98
|
if (!response.ok) throw new Error('pet state failed: ' + response.status)
|
|
107
|
-
const body = (await response.json()) as { pet?: { id?: unknown } }
|
|
99
|
+
const body = (await response.json()) as { pet?: { id?: unknown }; display?: { visible?: unknown } }
|
|
108
100
|
if (typeof body.pet?.id !== 'string') throw new Error('pet state has no selected pet')
|
|
109
|
-
|
|
101
|
+
// A Host without the display block predates the visibility switch; the pet shows.
|
|
102
|
+
return { petId: body.pet.id, visible: body.display?.visible !== false }
|
|
110
103
|
}
|
|
111
104
|
|
|
112
105
|
/** Bridges the 'pet' scope onto the card's staged form. */
|
|
@@ -118,9 +111,10 @@ export class PetSettingsCardController {
|
|
|
118
111
|
// without rebuilding the form.
|
|
119
112
|
private readonly petChoices: string[] = []
|
|
120
113
|
private readonly petLabels = new Map<string, string>()
|
|
121
|
-
private diagnostics: PetDiagnosticView[] = []
|
|
122
114
|
private selectedPetId: string | undefined
|
|
115
|
+
private selectedVisible: boolean | undefined
|
|
123
116
|
private stagedPetId: string | undefined
|
|
117
|
+
private stagedVisible: boolean | undefined
|
|
124
118
|
private savingPet = false
|
|
125
119
|
private petSaveFailed = false
|
|
126
120
|
private loaded = false
|
|
@@ -150,22 +144,10 @@ export class PetSettingsCardController {
|
|
|
150
144
|
this.pendingTimer = undefined
|
|
151
145
|
if (this.disposed) return
|
|
152
146
|
void this.loadPets()
|
|
153
|
-
void this.
|
|
154
|
-
void this.loadSelectedPet()
|
|
147
|
+
void this.loadPetState()
|
|
155
148
|
}, 0)
|
|
156
149
|
}
|
|
157
150
|
|
|
158
|
-
/** Fetch registry diagnostics once (soft-fail: an empty list on error). */
|
|
159
|
-
private async loadDiagnostics(): Promise<void> {
|
|
160
|
-
try {
|
|
161
|
-
this.diagnostics = await fetchPetDiagnostics()
|
|
162
|
-
if (this.disposed) return
|
|
163
|
-
this.store.set(this.projection())
|
|
164
|
-
} catch {
|
|
165
|
-
this.diagnostics = []
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
151
|
/** Resolve the registry choices once (retried a few times on failure). */
|
|
170
152
|
private async loadPets(): Promise<void> {
|
|
171
153
|
if (this.loaded || this.disposed) return
|
|
@@ -189,11 +171,12 @@ export class PetSettingsCardController {
|
|
|
189
171
|
}
|
|
190
172
|
}
|
|
191
173
|
|
|
192
|
-
private async
|
|
174
|
+
private async loadPetState(): Promise<void> {
|
|
193
175
|
try {
|
|
194
|
-
const
|
|
176
|
+
const state = await fetchPetState()
|
|
195
177
|
if (this.disposed) return
|
|
196
|
-
this.selectedPetId = petId
|
|
178
|
+
this.selectedPetId = state.petId
|
|
179
|
+
this.selectedVisible = state.visible
|
|
197
180
|
this.store.set(this.projection())
|
|
198
181
|
} catch {
|
|
199
182
|
// The Host form remains the authority when it is available. An absent
|
|
@@ -206,24 +189,58 @@ export class PetSettingsCardController {
|
|
|
206
189
|
return shell.available && !shell.exposed && this.selectedPetId !== undefined
|
|
207
190
|
}
|
|
208
191
|
|
|
209
|
-
|
|
192
|
+
/**
|
|
193
|
+
* The visibility the fallback switch renders: the staged draft when the user
|
|
194
|
+
* moved it, the persisted value otherwise. There is no user layer to override
|
|
195
|
+
* here, so the field is never marked overridden.
|
|
196
|
+
*/
|
|
197
|
+
private fallbackVisible(): CardFieldState {
|
|
198
|
+
const value = this.stagedVisible ?? this.selectedVisible
|
|
199
|
+
return { text: value === undefined ? '' : String(value), overridden: false, invalid: false }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Persist the staged selection and visibility through the pet API — the only
|
|
204
|
+
* writer available when the aggregate shell serves this card, since there is
|
|
205
|
+
* no Host settings form to mutate — then confirm the read-back before the
|
|
206
|
+
* drafts are cleared.
|
|
207
|
+
*/
|
|
208
|
+
private async saveFallback(): Promise<void> {
|
|
210
209
|
const petId = this.stagedPetId
|
|
211
|
-
|
|
210
|
+
const visible = this.stagedVisible
|
|
211
|
+
if (this.savingPet) return
|
|
212
|
+
if (petId === undefined && visible === undefined) return
|
|
213
|
+
if (petId !== undefined && !this.petChoices.includes(petId)) return
|
|
212
214
|
this.savingPet = true
|
|
213
215
|
this.petSaveFailed = false
|
|
214
216
|
this.store.set(this.projection())
|
|
215
217
|
try {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
218
|
+
if (petId !== undefined) {
|
|
219
|
+
const response = await fetch('/api/pet/set-pet', {
|
|
220
|
+
method: 'POST',
|
|
221
|
+
headers: { 'content-type': 'application/json' },
|
|
222
|
+
body: JSON.stringify({ petId }),
|
|
223
|
+
})
|
|
224
|
+
const result = (await response.json()) as { ok?: boolean; petId?: string }
|
|
225
|
+
if (!response.ok || result.ok !== true || result.petId !== petId) {
|
|
226
|
+
throw new Error('pet selection was not persisted')
|
|
227
|
+
}
|
|
224
228
|
}
|
|
225
|
-
|
|
229
|
+
if (visible !== undefined) {
|
|
230
|
+
const response = await fetch('/api/pet/set-visible', {
|
|
231
|
+
method: 'POST',
|
|
232
|
+
headers: { 'content-type': 'application/json' },
|
|
233
|
+
body: JSON.stringify({ visible }),
|
|
234
|
+
})
|
|
235
|
+
if (!response.ok) throw new Error('pet visibility was not persisted')
|
|
236
|
+
}
|
|
237
|
+
const persisted = await fetchPetState()
|
|
238
|
+
if (petId !== undefined && persisted.petId !== petId) throw new Error('pet selection was not persisted')
|
|
239
|
+
if (visible !== undefined && persisted.visible !== visible) throw new Error('pet visibility was not persisted')
|
|
240
|
+
this.selectedPetId = persisted.petId
|
|
241
|
+
this.selectedVisible = persisted.visible
|
|
226
242
|
if (this.stagedPetId === petId) this.stagedPetId = undefined
|
|
243
|
+
if (this.stagedVisible === visible) this.stagedVisible = undefined
|
|
227
244
|
} catch {
|
|
228
245
|
this.petSaveFailed = true
|
|
229
246
|
} finally {
|
|
@@ -241,7 +258,8 @@ export class PetSettingsCardController {
|
|
|
241
258
|
...(fallback ? {
|
|
242
259
|
exposed: true,
|
|
243
260
|
writable: true,
|
|
244
|
-
dirty: this.stagedPetId !== undefined && this.stagedPetId !== this.selectedPetId
|
|
261
|
+
dirty: this.stagedPetId !== undefined && this.stagedPetId !== this.selectedPetId
|
|
262
|
+
|| this.stagedVisible !== undefined && this.stagedVisible !== this.selectedVisible,
|
|
245
263
|
invalid: this.stagedPetId !== undefined && !this.petChoices.includes(this.stagedPetId),
|
|
246
264
|
saving: this.savingPet,
|
|
247
265
|
failed: this.petSaveFailed,
|
|
@@ -250,7 +268,7 @@ export class PetSettingsCardController {
|
|
|
250
268
|
petSelectionFallback: fallback,
|
|
251
269
|
enabled: this.form.field('enabled'),
|
|
252
270
|
decorationEnabled: this.form.field('decorationEnabled'),
|
|
253
|
-
visible: this.form.field('visible'),
|
|
271
|
+
visible: fallback ? this.fallbackVisible() : this.form.field('visible'),
|
|
254
272
|
size: this.form.field('size'),
|
|
255
273
|
right: this.form.field('right'),
|
|
256
274
|
bottom: this.form.field('bottom'),
|
|
@@ -261,7 +279,6 @@ export class PetSettingsCardController {
|
|
|
261
279
|
? { ...configuredPet, text: this.selectedPetId }
|
|
262
280
|
: configuredPet,
|
|
263
281
|
petChoices: this.petChoices.map(id => ({ value: id, label: this.petLabels.get(id) ?? id })),
|
|
264
|
-
petDiagnostics: this.diagnostics,
|
|
265
282
|
}
|
|
266
283
|
}
|
|
267
284
|
|
|
@@ -275,22 +292,25 @@ export class PetSettingsCardController {
|
|
|
275
292
|
hooks: { petSettingsCard: this.store },
|
|
276
293
|
edit: (field, value) => {
|
|
277
294
|
if (!this.fallback()) return actions.edit(field, value)
|
|
278
|
-
if (field
|
|
279
|
-
this.
|
|
295
|
+
if (field === 'petId') this.stagedPetId = value === '' ? undefined : value
|
|
296
|
+
else if (field === 'visible') this.stagedVisible = value === '' ? undefined : value === 'true'
|
|
297
|
+
else return
|
|
280
298
|
this.petSaveFailed = false
|
|
281
299
|
this.store.set(this.projection())
|
|
282
300
|
},
|
|
283
301
|
resetField: (field) => {
|
|
284
302
|
if (!this.fallback()) return actions.resetField(field)
|
|
285
|
-
if (field
|
|
286
|
-
this.
|
|
303
|
+
if (field === 'petId') this.stagedPetId = undefined
|
|
304
|
+
else if (field === 'visible') this.stagedVisible = undefined
|
|
305
|
+
else return
|
|
287
306
|
this.petSaveFailed = false
|
|
288
307
|
this.store.set(this.projection())
|
|
289
308
|
},
|
|
290
|
-
save: () => { if (this.fallback()) void this.
|
|
309
|
+
save: () => { if (this.fallback()) void this.saveFallback(); else actions.save() },
|
|
291
310
|
discard: () => {
|
|
292
311
|
if (!this.fallback()) return actions.discard()
|
|
293
312
|
this.stagedPetId = undefined
|
|
313
|
+
this.stagedVisible = undefined
|
|
294
314
|
this.petSaveFailed = false
|
|
295
315
|
this.store.set(this.projection())
|
|
296
316
|
},
|
|
@@ -378,17 +398,7 @@ export function PetSettingsCard(props: PetSettingsCardProps) {
|
|
|
378
398
|
onEdit={(text) => { props.edit('petId', text) }}
|
|
379
399
|
onReset={() => { props.resetField('petId') }}
|
|
380
400
|
/>
|
|
381
|
-
|
|
382
|
-
<li className={sectionCss.diagnostics} data-dsh-part="diagnostics">
|
|
383
|
-
<span className={sectionCss.diagnosticsTitle}>{t('settings.diagnosticsTitle')}</span>
|
|
384
|
-
<ul>
|
|
385
|
-
{state.petDiagnostics.map((diagnostic, index) => (
|
|
386
|
-
<li key={index} data-level={diagnostic.level}>{diagnostic.message}</li>
|
|
387
|
-
))}
|
|
388
|
-
</ul>
|
|
389
|
-
</li>
|
|
390
|
-
)}
|
|
391
|
-
{state.petSelectionFallback ? null : <BooleanField
|
|
401
|
+
<BooleanField
|
|
392
402
|
id="settings-pet-visible"
|
|
393
403
|
label={t('settings.visible')}
|
|
394
404
|
hint={t('settings.visibleHint')}
|
|
@@ -399,7 +409,7 @@ export function PetSettingsCard(props: PetSettingsCardProps) {
|
|
|
399
409
|
{...state.visible}
|
|
400
410
|
onEdit={(text) => { props.edit('visible', text) }}
|
|
401
411
|
onReset={() => { props.resetField('visible') }}
|
|
402
|
-
/>
|
|
412
|
+
/>
|
|
403
413
|
{state.petSelectionFallback ? null : <ValueField
|
|
404
414
|
id="settings-pet-size"
|
|
405
415
|
label={t('settings.size')}
|
package/src/client/locales.ts
CHANGED
|
@@ -57,7 +57,6 @@ export const zh = {
|
|
|
57
57
|
'pet.collapseSessions': '收起会话气泡',
|
|
58
58
|
// 一级设置页(settings.section 席位)。
|
|
59
59
|
'settings.title': '宠物',
|
|
60
|
-
'settings.diagnosticsTitle': '宠物目录诊断',
|
|
61
60
|
'settings.description': '选择宠物并调整它的显示布局。',
|
|
62
61
|
'settings.pet': '宠物',
|
|
63
62
|
'settings.petHint': '选择显示哪只宠物;每只宠物独立命名,可在宠物悬浮面板改名。',
|
|
@@ -143,7 +142,6 @@ export const en = {
|
|
|
143
142
|
'pet.collapseSessions': 'Collapse session bubbles',
|
|
144
143
|
// First-level settings section (the `settings.section` seat).
|
|
145
144
|
'settings.title': 'Pet',
|
|
146
|
-
'settings.diagnosticsTitle': 'Pet directory diagnostics',
|
|
147
145
|
'settings.description': 'Pick a pet and tune its display layout.',
|
|
148
146
|
'settings.pet': 'Pet',
|
|
149
147
|
'settings.petHint': 'Choose which pet shows. Names are stored per pet; rename from the pet hover panel.',
|
|
@@ -2,6 +2,14 @@ import { readFileSync } from 'node:fs'
|
|
|
2
2
|
import { describe, expect, it } from 'vitest'
|
|
3
3
|
|
|
4
4
|
const css = readFileSync(new URL('./pet.module.css', import.meta.url), 'utf8')
|
|
5
|
+
const settingsCss = readFileSync(new URL('./settings-section.module.css', import.meta.url), 'utf8')
|
|
6
|
+
|
|
7
|
+
describe('pet settings section css', () => {
|
|
8
|
+
it('carries no leftover registry diagnostics block after its removal', () => {
|
|
9
|
+
expect(settingsCss).not.toContain('.diagnostics')
|
|
10
|
+
expect(settingsCss).toContain('.sectionList')
|
|
11
|
+
})
|
|
12
|
+
})
|
|
5
13
|
|
|
6
14
|
describe('pet bubble typography (#1549)', () => {
|
|
7
15
|
it('scales the bubble text and padding from the sprite-supplied variable', () => {
|
|
@@ -3,26 +3,3 @@
|
|
|
3
3
|
margin: 0;
|
|
4
4
|
padding: 0;
|
|
5
5
|
}
|
|
6
|
-
|
|
7
|
-
/* Registry diagnostics block (pet-center M2 P7, issue #623). */
|
|
8
|
-
.diagnostics {
|
|
9
|
-
margin: 8px 0;
|
|
10
|
-
padding: 8px 12px;
|
|
11
|
-
border: 1px dashed var(--dsw-alias-border-l2);
|
|
12
|
-
border-radius: 8px;
|
|
13
|
-
font-size: 12px;
|
|
14
|
-
color: var(--dsw-alias-label-tertiary);
|
|
15
|
-
}
|
|
16
|
-
.diagnosticsTitle {
|
|
17
|
-
display: block;
|
|
18
|
-
font-weight: 600;
|
|
19
|
-
margin-bottom: 4px;
|
|
20
|
-
color: var(--dsw-alias-label-primary);
|
|
21
|
-
}
|
|
22
|
-
.diagnostics ul {
|
|
23
|
-
margin: 0;
|
|
24
|
-
padding-left: 16px;
|
|
25
|
-
}
|
|
26
|
-
.diagnostics li[data-level="error"] {
|
|
27
|
-
color: var(--dsw-alias-label-error, #c04848);
|
|
28
|
-
}
|
package/src/index.test.ts
CHANGED
|
@@ -117,3 +117,42 @@ describe('petSettingsSection', () => {
|
|
|
117
117
|
expect(section.petId).toBe('doro')
|
|
118
118
|
})
|
|
119
119
|
})
|
|
120
|
+
|
|
121
|
+
describe('petSettingsSection display persistence', () => {
|
|
122
|
+
/** The layout a user dragged the pet to, as its own pet.json holds it. */
|
|
123
|
+
const dragged = { visible: true, size: 160, right: 1428, bottom: 167, bubbleScale: 1 }
|
|
124
|
+
|
|
125
|
+
it('user keeps the dragged position when the profile never committed one', () => {
|
|
126
|
+
// Given a pet.json holding the layout the pet was dragged to
|
|
127
|
+
// And a row config that never set the display fields, so they read back as
|
|
128
|
+
// the schema defaults (the bottom-right corner)
|
|
129
|
+
// When the plugin starts and resolves its active settings
|
|
130
|
+
const section = petSettingsSection(Config({}), 'ouo-neko', dragged)
|
|
131
|
+
// Then the dragged layout stands instead of snapping back to those defaults
|
|
132
|
+
expect(section).toMatchObject({ right: 1428, bottom: 167 })
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('user commits a display field and the profile value outranks the persisted one', () => {
|
|
136
|
+
// Given a dragged layout and a profile layer that committed a right inset
|
|
137
|
+
const section = petSettingsSection(Config({}), 'ouo-neko', dragged, { right: 48 })
|
|
138
|
+
// Then the explicit choice wins and the untouched field keeps the dragged one
|
|
139
|
+
expect(section).toMatchObject({ right: 48, bottom: 167 })
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('user commits the schema default and the choice is not mistaken for unset', () => {
|
|
143
|
+
// Given a dragged layout and a profile layer that committed the default inset
|
|
144
|
+
const section = petSettingsSection(Config({}), 'ouo-neko', dragged, { right: 24 })
|
|
145
|
+
// Then the explicit (default-valued) choice stands over the persisted one
|
|
146
|
+
expect(section.right).toBe(24)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('user commits a layout at runtime and the result outranks the persisted one', () => {
|
|
150
|
+
// Given a live reference the Host committed into the running config
|
|
151
|
+
const resolved = Config({})
|
|
152
|
+
commitLive(resolved.right, 500)
|
|
153
|
+
// When the plugin resolves the section against an older persisted layout
|
|
154
|
+
const section = petSettingsSection(resolved, 'ouo-neko', dragged)
|
|
155
|
+
// Then the live commit wins and the persisted value fills the other fields
|
|
156
|
+
expect(section).toMatchObject({ right: 500, bottom: 167 })
|
|
157
|
+
})
|
|
158
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -16,10 +16,10 @@ import { Context, type Volatile } from '@deepseek-ai/cordis'
|
|
|
16
16
|
import type {} from '@deepseek-ai/dsh-settings'
|
|
17
17
|
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
18
18
|
import z from '@deepseek-ai/schemastery'
|
|
19
|
-
import { PetService, type PetConfig, type PetSettingsSection } from './service.ts'
|
|
19
|
+
import { PET_SETTINGS_NAMESPACE, PetService, type PetConfig, type PetSettingsSection } from './service.ts'
|
|
20
20
|
import { makePetRoutes } from './routes.ts'
|
|
21
21
|
import { loadPetRegistry, petPackageRoot } from './registry.ts'
|
|
22
|
-
import { BUBBLE_SCALE_MAX, BUBBLE_SCALE_MIN, DEFAULT_PET_ID, DISPLAY_INSET_MAX, DISPLAY_SIZE_MAX, DISPLAY_SIZE_MIN } from './persist.ts'
|
|
22
|
+
import { BUBBLE_SCALE_MAX, BUBBLE_SCALE_MIN, DEFAULT_PET_ID, DISPLAY_INSET_MAX, DISPLAY_SIZE_MAX, DISPLAY_SIZE_MIN, type PetDisplayConfig } from './persist.ts'
|
|
23
23
|
import { mountOnce } from './mount-once.ts'
|
|
24
24
|
|
|
25
25
|
export { PetService, MAX_SESSION_BUBBLES } from './service.ts'
|
|
@@ -203,29 +203,115 @@ function readLive<T>(field: LiveField<T> | undefined, fallback: T): T {
|
|
|
203
203
|
return typeof ref.get === 'function' ? ref.get() ?? fallback : field as T
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Resolve one display field of the section. The precedence is deliberate:
|
|
208
|
+
*
|
|
209
|
+
* 1. an explicit profile value (`committed`) — the user's own choice, and the
|
|
210
|
+
* only signal that carries a value equal to the schema default on purpose;
|
|
211
|
+
* 2. a live value the row committed at runtime, i.e. one that differs from the
|
|
212
|
+
* schema default, which is all an untouched field ever resolves to;
|
|
213
|
+
* 3. what the pet's own `pet.json` already holds (`persisted`), so a field the
|
|
214
|
+
* profile never touched keeps the position the user dragged instead of
|
|
215
|
+
* snapping back to the schema default on the next mount.
|
|
216
|
+
*
|
|
217
|
+
* Step 3 is what makes a drag survive a restart. The display fields carry
|
|
218
|
+
* schema defaults (`right: 24`, `bottom: 20` — the bottom-right corner), and a
|
|
219
|
+
* config that never set them reads back exactly like one that set them to the
|
|
220
|
+
* defaults, so without `persisted` the startup section resets the layout every
|
|
221
|
+
* time the profile commits none — which is what an aggregate install does.
|
|
222
|
+
* @param key - the display field to resolve.
|
|
223
|
+
* @param live - the row config's own field, absent when the entry declares none.
|
|
224
|
+
* @param persisted - the display the pet already shows (its own `pet.json`).
|
|
225
|
+
* @param committed - the profile layer's explicit display values, when known.
|
|
226
|
+
* @returns the value the pet runs with.
|
|
227
|
+
*/
|
|
228
|
+
function displayField<K extends keyof PetDisplayConfig>(
|
|
229
|
+
key: K,
|
|
230
|
+
live: LiveField<PetDisplayConfig[K]> | undefined,
|
|
231
|
+
persisted: Partial<PetDisplayConfig>,
|
|
232
|
+
committed: Partial<PetDisplayConfig>,
|
|
233
|
+
): PetDisplayConfig[K] {
|
|
234
|
+
const explicit = committed[key]
|
|
235
|
+
if (explicit !== undefined) return explicit
|
|
236
|
+
const value = readLive<PetDisplayConfig[K]>(live, PET_FORM_DEFAULTS[key])
|
|
237
|
+
if (value !== PET_FORM_DEFAULTS[key]) return value
|
|
238
|
+
const kept = persisted[key]
|
|
239
|
+
return kept === undefined ? value : kept
|
|
240
|
+
}
|
|
241
|
+
|
|
206
242
|
/**
|
|
207
243
|
* The settings section the pet runs with: the effective values of the row's
|
|
208
244
|
* own config. `fallbackPetId` covers a mount whose config names no pet at all
|
|
209
245
|
* (a direct mount outside a Loader) — under a Loader the schema default is
|
|
210
246
|
* always present, so the persisted selection stands whenever the config
|
|
211
|
-
* carries it.
|
|
247
|
+
* carries it. `persisted` and `committed` give the display fields the same
|
|
248
|
+
* treatment; see {@link displayField}.
|
|
212
249
|
* @param config - the effective config of the pet row.
|
|
213
250
|
* @param fallbackPetId - pet id to use when the config names none.
|
|
251
|
+
* @param persisted - the display the pet already shows (its own `pet.json`).
|
|
252
|
+
* @param committed - the profile layer's explicit display values, when known.
|
|
214
253
|
* @returns the resolved settings section.
|
|
215
254
|
*/
|
|
216
|
-
export function petSettingsSection(
|
|
255
|
+
export function petSettingsSection(
|
|
256
|
+
config: PetFormConfig,
|
|
257
|
+
fallbackPetId: string,
|
|
258
|
+
persisted: Partial<PetDisplayConfig> = {},
|
|
259
|
+
committed: Partial<PetDisplayConfig> = {},
|
|
260
|
+
): PetSettingsSection {
|
|
217
261
|
return {
|
|
218
|
-
visible:
|
|
219
|
-
size:
|
|
220
|
-
right:
|
|
221
|
-
bottom:
|
|
222
|
-
bubbleScale:
|
|
262
|
+
visible: displayField('visible', config.visible, persisted, committed),
|
|
263
|
+
size: displayField('size', config.size, persisted, committed),
|
|
264
|
+
right: displayField('right', config.right, persisted, committed),
|
|
265
|
+
bottom: displayField('bottom', config.bottom, persisted, committed),
|
|
266
|
+
bubbleScale: displayField('bubbleScale', config.bubbleScale, persisted, committed),
|
|
223
267
|
petId: readLive(config.petId, fallbackPetId),
|
|
224
268
|
enabled: readLive(config.enabled, PET_FORM_DEFAULTS.enabled),
|
|
225
269
|
decorationEnabled: readLive(config.decorationEnabled, PET_FORM_DEFAULTS.decorationEnabled),
|
|
226
270
|
}
|
|
227
271
|
}
|
|
228
272
|
|
|
273
|
+
/** The settings row ids a pet install is served under, most specific first. */
|
|
274
|
+
const PET_SETTINGS_ROW_IDS: readonly string[] = ['web-ui-pet', 'ui-pet', PET_SETTINGS_NAMESPACE]
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The fields this host half reads off one settings descriptor. The settings
|
|
278
|
+
* service serves one descriptor per plugin row (see `describe`), and a plugin's
|
|
279
|
+
* own config IS its settings page, so the two that matter here are the row's
|
|
280
|
+
* namespace and the profile layer's explicit values.
|
|
281
|
+
*/
|
|
282
|
+
interface ServedSettingsRow {
|
|
283
|
+
/** The profile entry's id, which is also this plugin's settings namespace. */
|
|
284
|
+
ns: string
|
|
285
|
+
/** The profile layer's explicit values; absent fields were never committed. */
|
|
286
|
+
user?: Partial<PetDisplayConfig>
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The settings row serving this plugin's own config, or undefined when the
|
|
291
|
+
* settings service is not mounted (a direct mount) or serves no row for the pet.
|
|
292
|
+
*
|
|
293
|
+
* Every settings write has to address this row's id, and that id is not the
|
|
294
|
+
* package's plugin name: an aggregate bundle renames child rows (`pet` becomes
|
|
295
|
+
* `web-ui-pet`). The browser half resolves the same id through its own
|
|
296
|
+
* `servedEntryId`, so both halves write to one namespace.
|
|
297
|
+
* @param ctx - the plugin's context.
|
|
298
|
+
* @returns the serving row, when there is one.
|
|
299
|
+
*/
|
|
300
|
+
function servedRow(ctx: Context): ServedSettingsRow | undefined {
|
|
301
|
+
try {
|
|
302
|
+
const settings = ctx.get('settings', false) as { describe(): readonly ServedSettingsRow[] } | undefined
|
|
303
|
+
if (settings === undefined) return undefined
|
|
304
|
+
const rows = settings.describe()
|
|
305
|
+
for (const id of PET_SETTINGS_ROW_IDS) {
|
|
306
|
+
const row = rows.find(candidate => candidate.ns === id)
|
|
307
|
+
if (row !== undefined) return row
|
|
308
|
+
}
|
|
309
|
+
return undefined
|
|
310
|
+
} catch {
|
|
311
|
+
return undefined
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
229
315
|
/** Register the pet service and its API + asset routes on the context. */
|
|
230
316
|
export const apply = mountOnce('@linxin666/dsh-pet', applyImpl)
|
|
231
317
|
|
|
@@ -244,13 +330,21 @@ function applyImpl(ctx: Context, config: PetPluginConfig = {}): void {
|
|
|
244
330
|
decorationEnabled: readLive(config.decorationEnabled, PET_FORM_DEFAULTS.decorationEnabled),
|
|
245
331
|
registry,
|
|
246
332
|
})
|
|
333
|
+
// The mirror back into the settings document (see syncSettingsFromPet) has to
|
|
334
|
+
// address the row that serves this plugin's config, which an aggregate bundle
|
|
335
|
+
// renames; the id is resolved lazily, once the composition serves it.
|
|
336
|
+
service.settingsNamespaceProvider = () => servedRow(ctx)?.ns
|
|
247
337
|
|
|
248
338
|
// The effective settings ARE this row's own config: the Host serves one form
|
|
249
339
|
// per profile entry from `Config` above, and every edit is committed into the
|
|
250
340
|
// running config, so the plugin re-reads the section here instead of holding
|
|
251
341
|
// a separate settings document (0.1.6 registered one through
|
|
252
|
-
// settings.installSection/register and re-resolved it on every change).
|
|
253
|
-
|
|
342
|
+
// settings.installSection/register and re-resolved it on every change). The
|
|
343
|
+
// display fields additionally read the persisted layout and the profile layer,
|
|
344
|
+
// so a mount with a config that never set them cannot reset a dragged
|
|
345
|
+
// position to the schema defaults (see displayField).
|
|
346
|
+
const current = (): PetSettingsSection =>
|
|
347
|
+
petSettingsSection(config, service.selectedPetId(), service.display(), servedRow(ctx)?.user)
|
|
254
348
|
// The browser half talks to the pet through same-origin JSON endpoints and
|
|
255
349
|
// loads each pet's atlas from the registry's own media route (RPC domains
|
|
256
350
|
// are platform-registered, so the pet serves its own API — the same
|