@dickpy/dsh-imagegen 1.5.2 → 1.5.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.md +23 -2
- package/lib/client.js +5803 -1942
- package/lib/client.js.map +1 -1
- package/lib/index.js +1151 -33
- package/package.json +1 -1
- package/src/canvas-store.ts +376 -0
- package/src/client/CanvasWorkspace.tsx +1569 -0
- package/src/client/ImageGenPanel.tsx +164 -96
- package/src/client/SettingsCard.tsx +182 -4
- package/src/client/api.ts +48 -1
- package/src/client/canvas-workspace.module.css +929 -0
- package/src/client/helpers.ts +71 -33
- package/src/client/index.ts +59 -7
- package/src/client/locales.ts +1452 -772
- package/src/client/panel.module.css +83 -33
- package/src/client/use-language.ts +14 -0
- package/src/engine.ts +302 -12
- package/src/gallery-store.ts +5 -0
- package/src/generation-runtime.ts +1 -0
- package/src/history-store.ts +5 -0
- package/src/index.ts +71 -4
- package/src/model-catalog.ts +10 -1
- package/src/presets.ts +15 -0
- package/src/protocol.ts +121 -1
- package/src/routes.ts +231 -1
- package/src/storage-sync.ts +105 -0
|
@@ -21,6 +21,8 @@ import type { ImageGenScope } from './settings-scope.ts'
|
|
|
21
21
|
import { describeModel } from '../model-catalog.ts'
|
|
22
22
|
import { IMAGE_MODEL_API, PRESETS_API, PROMPT_ENHANCE_API, USAGE_API, type ModelMapping, type PresetProviderView } from '../protocol.ts'
|
|
23
23
|
import type { ImageGenKey } from './locales.ts'
|
|
24
|
+
import { tt, type TranslateValues } from './helpers.ts'
|
|
25
|
+
import { useImageGenLanguageTick } from './use-language.ts'
|
|
24
26
|
import css from './settings-card.module.css'
|
|
25
27
|
|
|
26
28
|
/** The global (non-channel) fields this card's staged form edits. */
|
|
@@ -31,6 +33,14 @@ export interface ImageGenSettings {
|
|
|
31
33
|
promptApiUrl?: string
|
|
32
34
|
promptApiKey?: string
|
|
33
35
|
promptModel?: string
|
|
36
|
+
storageEnabled?: boolean
|
|
37
|
+
storageEndpoint?: string
|
|
38
|
+
storageRegion?: string
|
|
39
|
+
storagePrefix?: string
|
|
40
|
+
storageAccessKey?: string
|
|
41
|
+
storageSecretKey?: string
|
|
42
|
+
storageSyncGallery?: boolean
|
|
43
|
+
storageSyncHistory?: boolean
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
/** What the card renders. */
|
|
@@ -45,12 +55,29 @@ export interface ImageGenSettingsCardState extends CardShell {
|
|
|
45
55
|
promptApiUrl: CardFieldState
|
|
46
56
|
promptApiKey: CardFieldState
|
|
47
57
|
promptModel: CardFieldState
|
|
58
|
+
storageEnabled: CardFieldState
|
|
59
|
+
storageEndpoint: CardFieldState
|
|
60
|
+
storageRegion: CardFieldState
|
|
61
|
+
storagePrefix: CardFieldState
|
|
62
|
+
storageAccessKey: CardFieldState
|
|
63
|
+
storageSecretKey: CardFieldState
|
|
64
|
+
storageSyncGallery: CardFieldState
|
|
65
|
+
storageSyncHistory: CardFieldState
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Result of probing the configured object storage from the card. */
|
|
69
|
+
export interface StorageTestOutcome {
|
|
70
|
+
ok: boolean
|
|
71
|
+
ms?: number
|
|
72
|
+
message?: string
|
|
48
73
|
}
|
|
49
74
|
|
|
50
75
|
/** The registration-side face the card's slot entry injects. */
|
|
51
76
|
export interface ImageGenSettingsCardFace extends CardActions {
|
|
52
77
|
/** Channel staging actions (committed together with the card's save). */
|
|
53
78
|
channels: ChannelsFormActions
|
|
79
|
+
/** Save staged edits, then upload a probe object to the configured store. */
|
|
80
|
+
storageTest: () => Promise<StorageTestOutcome>
|
|
54
81
|
hooks: {
|
|
55
82
|
/** Card snapshot bound by the renderer as useImageGenSettingsCard. */
|
|
56
83
|
imageGenSettingsCard: SnapshotStore<ImageGenSettingsCardState>
|
|
@@ -71,8 +98,16 @@ export class ImageGenSettingsCardController {
|
|
|
71
98
|
textField('promptApiUrl'),
|
|
72
99
|
secretField('promptApiKey'),
|
|
73
100
|
textField('promptModel'),
|
|
101
|
+
booleanField('storageEnabled'),
|
|
102
|
+
textField('storageEndpoint'),
|
|
103
|
+
textField('storageRegion'),
|
|
104
|
+
textField('storagePrefix'),
|
|
105
|
+
textField('storageAccessKey'),
|
|
106
|
+
secretField('storageSecretKey'),
|
|
107
|
+
booleanField('storageSyncGallery'),
|
|
108
|
+
booleanField('storageSyncHistory'),
|
|
74
109
|
], {
|
|
75
|
-
secretSettled: () => this.scope.getSecretSetSnapshot(
|
|
110
|
+
secretSettled: (field) => this.scope.getSecretSetSnapshot(field),
|
|
76
111
|
})
|
|
77
112
|
this.channelsForm = new ChannelsForm(scope)
|
|
78
113
|
}
|
|
@@ -89,6 +124,14 @@ export class ImageGenSettingsCardController {
|
|
|
89
124
|
promptApiUrl: this.form.field('promptApiUrl'),
|
|
90
125
|
promptApiKey: this.form.field('promptApiKey'),
|
|
91
126
|
promptModel: this.form.field('promptModel'),
|
|
127
|
+
storageEnabled: this.form.field('storageEnabled'),
|
|
128
|
+
storageEndpoint: this.form.field('storageEndpoint'),
|
|
129
|
+
storageRegion: this.form.field('storageRegion'),
|
|
130
|
+
storagePrefix: this.form.field('storagePrefix'),
|
|
131
|
+
storageAccessKey: this.form.field('storageAccessKey'),
|
|
132
|
+
storageSecretKey: this.form.field('storageSecretKey'),
|
|
133
|
+
storageSyncGallery: this.form.field('storageSyncGallery'),
|
|
134
|
+
storageSyncHistory: this.form.field('storageSyncHistory'),
|
|
92
135
|
}
|
|
93
136
|
}
|
|
94
137
|
|
|
@@ -104,6 +147,19 @@ export class ImageGenSettingsCardController {
|
|
|
104
147
|
imageGenSettingsCard: cardStore,
|
|
105
148
|
},
|
|
106
149
|
channels: this.channelsForm.actions(),
|
|
150
|
+
// The probe needs the values the user is looking at, so staged edits are
|
|
151
|
+
// committed first; the host route then resolves the saved config itself.
|
|
152
|
+
storageTest: async (): Promise<StorageTestOutcome> => {
|
|
153
|
+
await this.form.save()
|
|
154
|
+
try {
|
|
155
|
+
const response = await fetch('/api/dsh-imagegen/storage/test', { method: 'POST' })
|
|
156
|
+
const body = await response.json() as { ok?: unknown; ms?: unknown; message?: unknown }
|
|
157
|
+
if (body.ok === true) return { ok: true, ms: typeof body.ms === 'number' ? body.ms : undefined }
|
|
158
|
+
return { ok: false, message: typeof body.message === 'string' ? body.message : `HTTP ${response.status}` }
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return { ok: false, message: error instanceof Error ? error.message : String(error) }
|
|
161
|
+
}
|
|
162
|
+
},
|
|
107
163
|
...this.form.actions(),
|
|
108
164
|
}
|
|
109
165
|
}
|
|
@@ -127,7 +183,11 @@ interface UsageCounters {
|
|
|
127
183
|
* @returns the card, or nothing while the namespace is still loading.
|
|
128
184
|
*/
|
|
129
185
|
export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
|
|
130
|
-
|
|
186
|
+
// The card renders through the plugin's own dictionary so the uiLanguage
|
|
187
|
+
// override applies here too — the host-locale props.t would only follow the
|
|
188
|
+
// DSH interface language.
|
|
189
|
+
const t = tt
|
|
190
|
+
useImageGenLanguageTick()
|
|
131
191
|
const state = props.useImageGenSettingsCard(snapshot => snapshot)
|
|
132
192
|
const [open, setOpen] = useState(false)
|
|
133
193
|
// Global-section local states (prompt enhancement etc.).
|
|
@@ -138,6 +198,9 @@ export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
|
|
|
138
198
|
const [manualPromptModel, setManualPromptModel] = useState('')
|
|
139
199
|
const [enhancementOpen, setEnhancementOpen] = useState(false)
|
|
140
200
|
const [promptApiOpen, setPromptApiOpen] = useState(false)
|
|
201
|
+
const [storageOpen, setStorageOpen] = useState(false)
|
|
202
|
+
const [storageTesting, setStorageTesting] = useState(false)
|
|
203
|
+
const [storageTestResult, setStorageTestResult] = useState<string | null>(null)
|
|
141
204
|
const [moreOpen, setMoreOpen] = useState(false)
|
|
142
205
|
// Channel list local states.
|
|
143
206
|
const [editingId, setEditingId] = useState<string | null>(null)
|
|
@@ -407,6 +470,121 @@ export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
|
|
|
407
470
|
</div> : null}
|
|
408
471
|
</section> : null}
|
|
409
472
|
|
|
473
|
+
<button type="button" className={css.disclosure} aria-expanded={storageOpen} onClick={() => { setStorageOpen(open => !open) }}>
|
|
474
|
+
<span>{t('settings.storageTitle')}</span>
|
|
475
|
+
<span aria-hidden="true">{storageOpen ? '⌃' : '⌄'}</span>
|
|
476
|
+
</button>
|
|
477
|
+
{storageOpen ? <div className={css.optionalContent}>
|
|
478
|
+
<BooleanField
|
|
479
|
+
id="dsh-imagegen-settings-storage-enabled"
|
|
480
|
+
label={t('settings.storageEnabled')}
|
|
481
|
+
hint={t('settings.storageHint')}
|
|
482
|
+
inheritLabel={t('settings.inherit')}
|
|
483
|
+
onLabel={t('settings.on')}
|
|
484
|
+
offLabel={t('settings.off')}
|
|
485
|
+
{...fieldProps}
|
|
486
|
+
{...state.storageEnabled}
|
|
487
|
+
onEdit={(text) => { props.edit('storageEnabled', text) }}
|
|
488
|
+
onReset={() => { props.resetField('storageEnabled') }}
|
|
489
|
+
/>
|
|
490
|
+
<ValueField
|
|
491
|
+
id="dsh-imagegen-settings-storage-endpoint"
|
|
492
|
+
label={t('settings.storageEndpoint')}
|
|
493
|
+
hint={t('settings.storageEndpointHint')}
|
|
494
|
+
placeholder="https://bucket-appid.cos.ap-guangzhou.myqcloud.com"
|
|
495
|
+
{...fieldProps}
|
|
496
|
+
{...state.storageEndpoint}
|
|
497
|
+
onEdit={(text) => { props.edit('storageEndpoint', text) }}
|
|
498
|
+
onReset={() => { props.resetField('storageEndpoint') }}
|
|
499
|
+
/>
|
|
500
|
+
<ValueField
|
|
501
|
+
id="dsh-imagegen-settings-storage-region"
|
|
502
|
+
label={t('settings.storageRegion')}
|
|
503
|
+
hint={t('settings.storageRegionHint')}
|
|
504
|
+
placeholder="ap-guangzhou"
|
|
505
|
+
{...fieldProps}
|
|
506
|
+
{...state.storageRegion}
|
|
507
|
+
onEdit={(text) => { props.edit('storageRegion', text) }}
|
|
508
|
+
onReset={() => { props.resetField('storageRegion') }}
|
|
509
|
+
/>
|
|
510
|
+
<ValueField
|
|
511
|
+
id="dsh-imagegen-settings-storage-prefix"
|
|
512
|
+
label={t('settings.storagePrefix')}
|
|
513
|
+
hint={t('settings.storagePrefixHint')}
|
|
514
|
+
placeholder="dsh-imagegen"
|
|
515
|
+
{...fieldProps}
|
|
516
|
+
{...state.storagePrefix}
|
|
517
|
+
onEdit={(text) => { props.edit('storagePrefix', text) }}
|
|
518
|
+
onReset={() => { props.resetField('storagePrefix') }}
|
|
519
|
+
/>
|
|
520
|
+
<ValueField
|
|
521
|
+
id="dsh-imagegen-settings-storage-accesskey"
|
|
522
|
+
label={t('settings.storageAccessKey')}
|
|
523
|
+
hint={t('settings.storageAccessKeyHint')}
|
|
524
|
+
placeholder="AKID…"
|
|
525
|
+
{...fieldProps}
|
|
526
|
+
{...state.storageAccessKey}
|
|
527
|
+
onEdit={(text) => { props.edit('storageAccessKey', text) }}
|
|
528
|
+
onReset={() => { props.resetField('storageAccessKey') }}
|
|
529
|
+
/>
|
|
530
|
+
<ValueField
|
|
531
|
+
id="dsh-imagegen-settings-storage-secretkey"
|
|
532
|
+
label={t('settings.storageSecretKey')}
|
|
533
|
+
hint={t('settings.storageSecretKeyHint')}
|
|
534
|
+
placeholder="…"
|
|
535
|
+
secret
|
|
536
|
+
{...fieldProps}
|
|
537
|
+
{...state.storageSecretKey}
|
|
538
|
+
overridden={false}
|
|
539
|
+
onEdit={(text) => { props.edit('storageSecretKey', text) }}
|
|
540
|
+
onReset={() => { props.resetField('storageSecretKey') }}
|
|
541
|
+
/>
|
|
542
|
+
<BooleanField
|
|
543
|
+
id="dsh-imagegen-settings-storage-gallery"
|
|
544
|
+
label={t('settings.storageSyncGallery')}
|
|
545
|
+
hint={t('settings.storageSyncGalleryHint')}
|
|
546
|
+
inheritLabel={t('settings.inherit')}
|
|
547
|
+
onLabel={t('settings.on')}
|
|
548
|
+
offLabel={t('settings.off')}
|
|
549
|
+
{...fieldProps}
|
|
550
|
+
{...state.storageSyncGallery}
|
|
551
|
+
onEdit={(text) => { props.edit('storageSyncGallery', text) }}
|
|
552
|
+
onReset={() => { props.resetField('storageSyncGallery') }}
|
|
553
|
+
/>
|
|
554
|
+
<BooleanField
|
|
555
|
+
id="dsh-imagegen-settings-storage-history"
|
|
556
|
+
label={t('settings.storageSyncHistory')}
|
|
557
|
+
hint={t('settings.storageSyncHistoryHint')}
|
|
558
|
+
inheritLabel={t('settings.inherit')}
|
|
559
|
+
onLabel={t('settings.on')}
|
|
560
|
+
offLabel={t('settings.off')}
|
|
561
|
+
{...fieldProps}
|
|
562
|
+
{...state.storageSyncHistory}
|
|
563
|
+
onEdit={(text) => { props.edit('storageSyncHistory', text) }}
|
|
564
|
+
onReset={() => { props.resetField('storageSyncHistory') }}
|
|
565
|
+
/>
|
|
566
|
+
<div className={css.modelSummary}>
|
|
567
|
+
<button
|
|
568
|
+
type="button"
|
|
569
|
+
className={css.addModel}
|
|
570
|
+
disabled={disabled || storageTesting}
|
|
571
|
+
onClick={() => {
|
|
572
|
+
setStorageTesting(true)
|
|
573
|
+
setStorageTestResult(null)
|
|
574
|
+
void props.storageTest().then(outcome => {
|
|
575
|
+
setStorageTestResult(outcome.ok
|
|
576
|
+
? t('settings.storageTestOk', { ms: outcome.ms ?? 0 })
|
|
577
|
+
: t('settings.storageTestFailed', { error: outcome.message ?? 'error' }))
|
|
578
|
+
}).finally(() => { setStorageTesting(false) })
|
|
579
|
+
}}
|
|
580
|
+
>
|
|
581
|
+
{storageTesting ? t('settings.storageTesting') : t('settings.storageTest')}
|
|
582
|
+
</button>
|
|
583
|
+
{storageTestResult !== null ? <p className={css.failed} role="status">{storageTestResult}</p> : null}
|
|
584
|
+
</div>
|
|
585
|
+
<p className={css.hint}>{t('settings.storageKeyHint')}</p>
|
|
586
|
+
</div> : null}
|
|
587
|
+
|
|
410
588
|
<button type="button" className={css.disclosure} aria-expanded={moreOpen} onClick={() => { setMoreOpen(open => !open) }}>
|
|
411
589
|
<span>{t('settings.moreOptions')}</span>
|
|
412
590
|
<span aria-hidden="true">{moreOpen ? '⌃' : '⌄'}</span>
|
|
@@ -445,8 +623,8 @@ export function ImageGenSettingsCard(props: ImageGenSettingsCardProps) {
|
|
|
445
623
|
offLabel={t('settings.off')}
|
|
446
624
|
{...fieldProps}
|
|
447
625
|
{...state.allowAgentImageGeneration}
|
|
448
|
-
onEdit={(text) => { props.edit('
|
|
449
|
-
onReset={() => { props.resetField('
|
|
626
|
+
onEdit={(text) => { props.edit('enabled', text) }}
|
|
627
|
+
onReset={() => { props.resetField('enabled') }}
|
|
450
628
|
/>
|
|
451
629
|
</div> : null}
|
|
452
630
|
<div className={css.footer}>
|
package/src/client/api.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* data access path the panel uses — plain fetch, same origin.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { CONVERSATION_IMAGE_API, GALLERY_API, GENERATE_API, HISTORY_API, PROMPT_ENHANCE_API, TASK_API, TEMPLATE_FAVORITES_API, TEMPLATES_API, UPDATE_API, type GenerateRequest, type GenerateResult, type GenerationTask, type HistoryEntry, type HistoryEntryInput, type TemplateCase, type TemplateFavorite, type TemplateListResult, type TemplateRefreshResult, type TemplateSample, type UpdateInfo } from '../protocol.ts'
|
|
6
|
+
import { CANVAS_API, CONVERSATION_IMAGE_API, DATA_FOLDER_API, GALLERY_API, GENERATE_API, HISTORY_API, PROMPT_ENHANCE_API, STORAGE_API, TASK_API, TEMPLATE_FAVORITES_API, TEMPLATES_API, UPDATE_API, type CanvasAssetRef, type CanvasDocument, type CanvasSummary, type GenerateRequest, type GenerateResult, type GenerationTask, type HistoryEntry, type HistoryEntryInput, type TemplateCase, type TemplateFavorite, type TemplateListResult, type TemplateRefreshResult, type TemplateSample, type UpdateInfo } from '../protocol.ts'
|
|
7
7
|
|
|
8
8
|
/** Error carrying the route's JSON error message. */
|
|
9
9
|
export class ImageGenApiError extends Error {
|
|
@@ -181,6 +181,41 @@ export class ImageGenApi {
|
|
|
181
181
|
return (await readEnvelope<{ ok: true; entries: HistoryEntry[] }>(response)).entries
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
async canvasList(): Promise<CanvasSummary[]> {
|
|
185
|
+
const response = await fetch(CANVAS_API.list, { method: 'POST' })
|
|
186
|
+
return (await readEnvelope<{ ok: true; projects: CanvasSummary[] }>(response)).projects
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async canvasCreate(title?: string): Promise<CanvasDocument> {
|
|
190
|
+
const response = await fetch(CANVAS_API.create, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ title }) })
|
|
191
|
+
return (await readEnvelope<{ ok: true; document: CanvasDocument }>(response)).document
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async canvasRead(id: string): Promise<CanvasDocument> {
|
|
195
|
+
const response = await fetch(CANVAS_API.read, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id }) })
|
|
196
|
+
return (await readEnvelope<{ ok: true; document: CanvasDocument }>(response)).document
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async canvasSave(document: CanvasDocument, expectedRevision: number): Promise<CanvasDocument> {
|
|
200
|
+
const response = await fetch(CANVAS_API.save, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ document, expectedRevision }) })
|
|
201
|
+
return (await readEnvelope<{ ok: true; document: CanvasDocument }>(response)).document
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async canvasRemove(id: string): Promise<CanvasSummary[]> {
|
|
205
|
+
const response = await fetch(CANVAS_API.remove, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id }) })
|
|
206
|
+
return (await readEnvelope<{ ok: true; projects: CanvasSummary[] }>(response)).projects
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async canvasUpload(dataUrl: string, width: number, height: number, meta: { origin?: string; originId?: string; entryId?: string; imageIndex?: number } = {}): Promise<CanvasAssetRef> {
|
|
210
|
+
const response = await fetch(CANVAS_API.assetUpload, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ dataUrl, width, height, ...meta }) })
|
|
211
|
+
return (await readEnvelope<{ ok: true; asset: CanvasAssetRef }>(response)).asset
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async canvasImport(source: 'history' | 'gallery', entryId: string, imageIndex: number, width: number, height: number): Promise<CanvasAssetRef> {
|
|
215
|
+
const response = await fetch(CANVAS_API.assetImport, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ source, entryId, imageIndex, width, height }) })
|
|
216
|
+
return (await readEnvelope<{ ok: true; asset: CanvasAssetRef }>(response)).asset
|
|
217
|
+
}
|
|
218
|
+
|
|
184
219
|
/** Fetch one template source's list (bundled snapshot or refreshed copy). */
|
|
185
220
|
async templatesList(sourceId: string): Promise<TemplateListResult> {
|
|
186
221
|
const response = await fetch(TEMPLATES_API.list, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ source: sourceId }) })
|
|
@@ -214,6 +249,18 @@ export class ImageGenApi {
|
|
|
214
249
|
return (await readEnvelope<{ ok: true; favorites: TemplateFavorite[] }>(response)).favorites
|
|
215
250
|
}
|
|
216
251
|
|
|
252
|
+
/** Reveal the host data directory (saved images) in the OS file manager. */
|
|
253
|
+
async openDataFolder(): Promise<{ ok: boolean; path?: string; message?: string }> {
|
|
254
|
+
const response = await fetch(DATA_FOLDER_API, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({}) })
|
|
255
|
+
return await readEnvelope<{ ok: true; path: string }>(response)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Probe the configured S3-compatible object storage with a small upload. */
|
|
259
|
+
async storageTest(): Promise<{ ok: boolean; ms?: number; key?: string; message?: string }> {
|
|
260
|
+
const response = await fetch(STORAGE_API.test, { method: 'POST' })
|
|
261
|
+
return await readEnvelope<{ ok: true; ms: number; key: string }>(response)
|
|
262
|
+
}
|
|
263
|
+
|
|
217
264
|
/** Star one template (the host keeps a full case snapshot). */
|
|
218
265
|
async favoritesAdd(sourceId: string, item: TemplateCase): Promise<TemplateFavorite[]> {
|
|
219
266
|
const response = await fetch(TEMPLATE_FAVORITES_API.add, {
|