@huaqiu/dsh-tool-plugin-update 0.0.0-stage → 0.5.21
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/LICENSE +21 -0
- package/cordis.patch.yml +10 -0
- package/lib/client.js +1006 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.mts +32 -0
- package/lib/index.mjs +178 -0
- package/package.json +50 -3
- package/src/client/api.ts +94 -0
- package/src/client/copy.ts +92 -0
- package/src/client/index.ts +508 -0
- package/src/client/update-view.ts +301 -0
- package/src/index.ts +108 -0
- package/src/routes.ts +149 -0
- package/README.md +0 -3
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@huaqiu/dsh-tool-plugin-update` — browser half.
|
|
3
|
+
*
|
|
4
|
+
* Mounts one sidebar toolbutton in the Huaqiu family block ("插件更新") with a
|
|
5
|
+
* numeric badge equal to the actionable plugin-update count (spec §6.3/§7.1:
|
|
6
|
+
* only compatible `update` + `install` items after host/HQ-Edge filtering and
|
|
7
|
+
* after excluding the ignored offer; runtime updates are NOT counted). Click
|
|
8
|
+
* opens the review/confirm/ignore modal.
|
|
9
|
+
*
|
|
10
|
+
* The authoritative state is the hq-edge report fetched through the plugin's
|
|
11
|
+
* same-origin forwarding routes. A tiny in-memory store owns the latest report
|
|
12
|
+
* + installing flag; the sidebar badge and the modal subscribe to it. No
|
|
13
|
+
* timers beyond the install-poll and an idle 60 s refresh.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import {
|
|
18
|
+
HUAQIU_SIDEBAR_ENTRY_FAMILY,
|
|
19
|
+
detectHostLocale,
|
|
20
|
+
mountSidebarEntry,
|
|
21
|
+
observeHostLocale,
|
|
22
|
+
type HostLocale,
|
|
23
|
+
} from '@huaqiu/dsh-sidebar-entry'
|
|
24
|
+
import { checkUpdates, fetchReport, type PluginUpdateReport } from './api.js'
|
|
25
|
+
import { COPY } from './copy.js'
|
|
26
|
+
import { openUpdateView, type UpdateStoreLike } from './update-view.js'
|
|
27
|
+
|
|
28
|
+
/** Stable data attributes identifying the injected entry row. */
|
|
29
|
+
export const UPDATE_ENTRY_SELECTOR = '[data-hq-plugin-update-entry]'
|
|
30
|
+
export const UPDATE_ENTRY_ATTRIBUTE = 'data-hq-plugin-update-entry'
|
|
31
|
+
|
|
32
|
+
/** L2 semantic plugin id: rows carry data-dsh-plugin + data-dsh-part. */
|
|
33
|
+
const PLUGIN_ID = 'huaqiu-plugin-update'
|
|
34
|
+
|
|
35
|
+
/** Row class names (defined by {@link ENTRY_CSS_TEXT}). */
|
|
36
|
+
const ENTRY_CSS: Record<string, string> = {
|
|
37
|
+
entry: 'hqpu-sidebar-entry',
|
|
38
|
+
entryIcon: 'hqpu-sidebar-entry__icon',
|
|
39
|
+
entryLabel: 'hqpu-sidebar-entry__label',
|
|
40
|
+
entryBadge: 'hqpu-sidebar-entry__badge',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Localized row copy: zh is the fallback, matching the family convention. */
|
|
44
|
+
function entryCopy(lang: HostLocale | undefined): { label: string; tooltip: string } {
|
|
45
|
+
return lang === 'en'
|
|
46
|
+
? { label: COPY.en.entryLabel, tooltip: COPY.en.entryTooltip }
|
|
47
|
+
: { label: COPY.zh.entryLabel, tooltip: COPY.zh.entryTooltip }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Inline update glyph — `currentColor`, 16×16 like the shell's nav glyphs. */
|
|
51
|
+
export const UPDATE_ICON =
|
|
52
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
|
53
|
+
<g fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">
|
|
54
|
+
<path d="M2.5 8a5.5 5.5 0 1 1 1.61 3.89" />
|
|
55
|
+
<path d="M2.5 8V4.75M2.5 8h3.25" />
|
|
56
|
+
</g>
|
|
57
|
+
</svg>`
|
|
58
|
+
|
|
59
|
+
/** Sidebar entry styles — matched to the family rows (same inset/padding/gap
|
|
60
|
+
* as the shell's own Plugins entry). The badge is a 16px rounded pill. */
|
|
61
|
+
const ENTRY_CSS_TEXT = `
|
|
62
|
+
.hqpu-sidebar-entry {
|
|
63
|
+
box-sizing: border-box;
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
gap: 8px;
|
|
67
|
+
width: 100%;
|
|
68
|
+
margin: 0 2px;
|
|
69
|
+
min-height: 36px;
|
|
70
|
+
padding: 7px 8px;
|
|
71
|
+
background: transparent;
|
|
72
|
+
border: none;
|
|
73
|
+
border-radius: 12px;
|
|
74
|
+
color: var(--dsw-alias-label-primary);
|
|
75
|
+
cursor: pointer;
|
|
76
|
+
font: inherit;
|
|
77
|
+
line-height: 22px;
|
|
78
|
+
text-align: left;
|
|
79
|
+
white-space: nowrap;
|
|
80
|
+
}
|
|
81
|
+
.hqpu-sidebar-entry:hover {
|
|
82
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
83
|
+
}
|
|
84
|
+
.hqpu-sidebar-entry[data-active] {
|
|
85
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
86
|
+
color: var(--dsw-alias-label-primary);
|
|
87
|
+
}
|
|
88
|
+
.hqpu-sidebar-entry__icon {
|
|
89
|
+
display: inline-flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
justify-content: center;
|
|
92
|
+
flex: none;
|
|
93
|
+
}
|
|
94
|
+
.hqpu-sidebar-entry__icon svg {
|
|
95
|
+
display: block;
|
|
96
|
+
width: 16px;
|
|
97
|
+
height: 16px;
|
|
98
|
+
}
|
|
99
|
+
.hqpu-sidebar-entry__label {
|
|
100
|
+
min-width: 0;
|
|
101
|
+
overflow: hidden;
|
|
102
|
+
text-overflow: ellipsis;
|
|
103
|
+
flex: 1;
|
|
104
|
+
}
|
|
105
|
+
.hqpu-sidebar-entry__badge {
|
|
106
|
+
flex: none;
|
|
107
|
+
min-width: 16px;
|
|
108
|
+
height: 16px;
|
|
109
|
+
padding: 0 5px;
|
|
110
|
+
border-radius: 8px;
|
|
111
|
+
background: var(--dsw-alias-bg-badge, #e5484d);
|
|
112
|
+
color: #fff;
|
|
113
|
+
font-size: 11px;
|
|
114
|
+
line-height: 16px;
|
|
115
|
+
text-align: center;
|
|
116
|
+
}
|
|
117
|
+
/* Collapsed rail: icon-only, centered — same 36px square the shell uses. */
|
|
118
|
+
[data-sidebar-collapsed] .hqpu-sidebar-entry {
|
|
119
|
+
justify-content: center;
|
|
120
|
+
width: 36px;
|
|
121
|
+
height: 36px;
|
|
122
|
+
margin: 0;
|
|
123
|
+
padding: 0;
|
|
124
|
+
}
|
|
125
|
+
[data-sidebar-collapsed] .hqpu-sidebar-entry__icon svg {
|
|
126
|
+
width: 18px;
|
|
127
|
+
height: 18px;
|
|
128
|
+
}
|
|
129
|
+
[data-sidebar-collapsed] .hqpu-sidebar-entry__label,
|
|
130
|
+
[data-sidebar-collapsed] .hqpu-sidebar-entry__badge {
|
|
131
|
+
display: none;
|
|
132
|
+
}
|
|
133
|
+
`
|
|
134
|
+
|
|
135
|
+
/** Modal styles (hqpu-*). Plain overlay + panel, aligned to the shell's
|
|
136
|
+
* dark/light aliases. */
|
|
137
|
+
const VIEW_CSS_TEXT = `
|
|
138
|
+
.hqpu-overlay {
|
|
139
|
+
position: fixed;
|
|
140
|
+
inset: 0;
|
|
141
|
+
z-index: 1000;
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
justify-content: center;
|
|
145
|
+
background: rgba(0, 0, 0, 0.45);
|
|
146
|
+
}
|
|
147
|
+
.hqpu-panel {
|
|
148
|
+
box-sizing: border-box;
|
|
149
|
+
width: min(640px, calc(100vw - 48px));
|
|
150
|
+
max-height: min(80vh, 720px);
|
|
151
|
+
display: flex;
|
|
152
|
+
flex-direction: column;
|
|
153
|
+
border-radius: 16px;
|
|
154
|
+
background: var(--dsw-alias-bg-surface, #fff);
|
|
155
|
+
color: var(--dsw-alias-label-primary, #1f2329);
|
|
156
|
+
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.24);
|
|
157
|
+
overflow: hidden;
|
|
158
|
+
}
|
|
159
|
+
.hqpu-header {
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
gap: 8px;
|
|
163
|
+
padding: 14px 16px;
|
|
164
|
+
border-bottom: 1px solid var(--dsw-alias-line-border, rgba(0,0,0,.08));
|
|
165
|
+
}
|
|
166
|
+
.hqpu-title {
|
|
167
|
+
flex: 1;
|
|
168
|
+
font-size: 15px;
|
|
169
|
+
font-weight: 600;
|
|
170
|
+
}
|
|
171
|
+
.hqpu-body {
|
|
172
|
+
overflow: auto;
|
|
173
|
+
padding: 16px;
|
|
174
|
+
display: flex;
|
|
175
|
+
flex-direction: column;
|
|
176
|
+
gap: 16px;
|
|
177
|
+
}
|
|
178
|
+
.hqpu-section {
|
|
179
|
+
display: flex;
|
|
180
|
+
flex-direction: column;
|
|
181
|
+
gap: 8px;
|
|
182
|
+
}
|
|
183
|
+
.hqpu-manifest {
|
|
184
|
+
display: flex;
|
|
185
|
+
align-items: center;
|
|
186
|
+
gap: 8px;
|
|
187
|
+
font-size: 12px;
|
|
188
|
+
color: var(--dsw-alias-label-secondary, #646a73);
|
|
189
|
+
}
|
|
190
|
+
.hqpu-manifest-value {
|
|
191
|
+
font-weight: 600;
|
|
192
|
+
color: var(--dsw-alias-label-primary, #1f2329);
|
|
193
|
+
}
|
|
194
|
+
.hqpu-list {
|
|
195
|
+
list-style: none;
|
|
196
|
+
margin: 0;
|
|
197
|
+
padding: 0;
|
|
198
|
+
display: flex;
|
|
199
|
+
flex-direction: column;
|
|
200
|
+
gap: 6px;
|
|
201
|
+
}
|
|
202
|
+
.hqpu-item {
|
|
203
|
+
display: grid;
|
|
204
|
+
grid-template-columns: 1fr auto auto;
|
|
205
|
+
gap: 8px;
|
|
206
|
+
align-items: center;
|
|
207
|
+
padding: 8px 10px;
|
|
208
|
+
border-radius: 10px;
|
|
209
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,.04));
|
|
210
|
+
font-size: 13px;
|
|
211
|
+
}
|
|
212
|
+
.hqpu-item-name {
|
|
213
|
+
font-weight: 500;
|
|
214
|
+
overflow: hidden;
|
|
215
|
+
text-overflow: ellipsis;
|
|
216
|
+
}
|
|
217
|
+
.hqpu-item-version {
|
|
218
|
+
color: var(--dsw-alias-label-secondary, #646a73);
|
|
219
|
+
font-variant-numeric: tabular-nums;
|
|
220
|
+
}
|
|
221
|
+
.hqpu-item-state {
|
|
222
|
+
padding: 1px 8px;
|
|
223
|
+
border-radius: 999px;
|
|
224
|
+
font-size: 11px;
|
|
225
|
+
line-height: 18px;
|
|
226
|
+
}
|
|
227
|
+
.hqpu-state-current { background: rgba(0,0,0,.06); color: var(--dsw-alias-label-secondary, #646a73); }
|
|
228
|
+
.hqpu-state-update, .hqpu-state-install { background: rgba(229,72,77,.12); color: #d92d33; }
|
|
229
|
+
.hqpu-state-ignored { background: rgba(0,0,0,.06); color: var(--dsw-alias-label-secondary, #646a73); }
|
|
230
|
+
.hqpu-state-blocked { background: rgba(229,72,77,.12); color: #d92d33; }
|
|
231
|
+
.hqpu-item-reason {
|
|
232
|
+
grid-column: 1 / -1;
|
|
233
|
+
font-size: 12px;
|
|
234
|
+
color: var(--dsw-alias-label-tertiary, #8f959e);
|
|
235
|
+
}
|
|
236
|
+
.hqpu-ignored-banner {
|
|
237
|
+
display: flex;
|
|
238
|
+
align-items: center;
|
|
239
|
+
gap: 8px;
|
|
240
|
+
padding: 8px 10px;
|
|
241
|
+
border-radius: 10px;
|
|
242
|
+
background: rgba(120,120,120,.1);
|
|
243
|
+
font-size: 12px;
|
|
244
|
+
color: var(--dsw-alias-label-secondary, #646a73);
|
|
245
|
+
}
|
|
246
|
+
.hqpu-empty {
|
|
247
|
+
padding: 24px 0;
|
|
248
|
+
text-align: center;
|
|
249
|
+
color: var(--dsw-alias-label-tertiary, #8f959e);
|
|
250
|
+
font-size: 13px;
|
|
251
|
+
}
|
|
252
|
+
.hqpu-runtime {
|
|
253
|
+
border-top: 1px solid var(--dsw-alias-line-border, rgba(0,0,0,.08));
|
|
254
|
+
padding-top: 12px;
|
|
255
|
+
}
|
|
256
|
+
.hqpu-runtime-title {
|
|
257
|
+
font-size: 13px;
|
|
258
|
+
font-weight: 600;
|
|
259
|
+
}
|
|
260
|
+
.hqpu-runtime-line {
|
|
261
|
+
font-size: 12px;
|
|
262
|
+
color: var(--dsw-alias-label-secondary, #646a73);
|
|
263
|
+
}
|
|
264
|
+
.hqpu-runtime-note {
|
|
265
|
+
font-size: 11px;
|
|
266
|
+
color: var(--dsw-alias-label-tertiary, #8f959e);
|
|
267
|
+
}
|
|
268
|
+
.hqpu-runtime-error {
|
|
269
|
+
font-size: 12px;
|
|
270
|
+
color: #d92d33;
|
|
271
|
+
}
|
|
272
|
+
.hqpu-actions {
|
|
273
|
+
display: flex;
|
|
274
|
+
align-items: center;
|
|
275
|
+
gap: 8px;
|
|
276
|
+
}
|
|
277
|
+
.hqpu-btn {
|
|
278
|
+
box-sizing: border-box;
|
|
279
|
+
padding: 6px 14px;
|
|
280
|
+
border-radius: 8px;
|
|
281
|
+
border: 1px solid transparent;
|
|
282
|
+
background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,.06));
|
|
283
|
+
color: var(--dsw-alias-label-primary, #1f2329);
|
|
284
|
+
font: inherit;
|
|
285
|
+
font-size: 13px;
|
|
286
|
+
cursor: pointer;
|
|
287
|
+
}
|
|
288
|
+
.hqpu-btn-primary {
|
|
289
|
+
background: #e5484d;
|
|
290
|
+
color: #fff;
|
|
291
|
+
}
|
|
292
|
+
.hqpu-btn-primary:disabled {
|
|
293
|
+
opacity: 0.5;
|
|
294
|
+
cursor: not-allowed;
|
|
295
|
+
}
|
|
296
|
+
.hqpu-btn-ghost {
|
|
297
|
+
background: transparent;
|
|
298
|
+
border-color: var(--dsw-alias-line-border, rgba(0,0,0,.12));
|
|
299
|
+
}
|
|
300
|
+
.hqpu-btn-small {
|
|
301
|
+
padding: 2px 10px;
|
|
302
|
+
font-size: 12px;
|
|
303
|
+
}
|
|
304
|
+
.hqpu-installing {
|
|
305
|
+
font-size: 13px;
|
|
306
|
+
color: var(--dsw-alias-label-secondary, #646a73);
|
|
307
|
+
}
|
|
308
|
+
.hqpu-error {
|
|
309
|
+
font-size: 13px;
|
|
310
|
+
color: #d92d33;
|
|
311
|
+
}
|
|
312
|
+
`
|
|
313
|
+
|
|
314
|
+
const STYLE_TAG_ID = 'data-plugin-css="huaqiu-plugin-update"'
|
|
315
|
+
|
|
316
|
+
/** Inject styles once (idempotent). */
|
|
317
|
+
export function injectUpdateStyles(): void {
|
|
318
|
+
if (typeof document === 'undefined') return
|
|
319
|
+
if (document.querySelector(`style[${STYLE_TAG_ID}]`) !== null) return
|
|
320
|
+
const tag = document.createElement('style')
|
|
321
|
+
tag.dataset.pluginCss = 'huaqiu-plugin-update'
|
|
322
|
+
tag.textContent = `${ENTRY_CSS_TEXT}\n${VIEW_CSS_TEXT}`
|
|
323
|
+
document.head.appendChild(tag)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Minimal structural client context (dsh-client-runtime provides these). */
|
|
327
|
+
export interface ClientContext {
|
|
328
|
+
get<T = unknown>(name: string): T | undefined
|
|
329
|
+
effect?(fn: () => (() => void) | void, label?: string): void
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** In-memory update-report store shared by the badge and the modal. */
|
|
333
|
+
class UpdateStore implements UpdateStoreLike {
|
|
334
|
+
private current: PluginUpdateReport | null = null
|
|
335
|
+
private installingFlag = false
|
|
336
|
+
private readonly listeners = new Set<() => void>()
|
|
337
|
+
private refreshTimer: number | null = null
|
|
338
|
+
|
|
339
|
+
report(): PluginUpdateReport | null {
|
|
340
|
+
return this.current
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
installing(): boolean {
|
|
344
|
+
return this.installingFlag
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
subscribe(listener: () => void): () => void {
|
|
348
|
+
this.listeners.add(listener)
|
|
349
|
+
return () => {
|
|
350
|
+
this.listeners.delete(listener)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Badge count per spec §6.3: actionable `update` + `install`, excluding the
|
|
355
|
+
* ignored offer (the server already folds ignore into item states; keep the
|
|
356
|
+
* client-side filter as a defensive second pass). */
|
|
357
|
+
badgeCount(): number {
|
|
358
|
+
const report = this.current
|
|
359
|
+
if (report === null) return 0
|
|
360
|
+
return report.items.filter(
|
|
361
|
+
(item) => item.state === 'update' || item.state === 'install',
|
|
362
|
+
).length
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private emit(): void {
|
|
366
|
+
for (const listener of [...this.listeners]) {
|
|
367
|
+
try {
|
|
368
|
+
listener()
|
|
369
|
+
} catch {
|
|
370
|
+
/* one bad listener must not strand the rest */
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async refresh(): Promise<PluginUpdateReport | null> {
|
|
376
|
+
try {
|
|
377
|
+
const report = await fetchReport()
|
|
378
|
+
this.current = report
|
|
379
|
+
this.emit()
|
|
380
|
+
return report
|
|
381
|
+
} catch {
|
|
382
|
+
return this.current
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async check(): Promise<PluginUpdateReport | null> {
|
|
387
|
+
try {
|
|
388
|
+
const report = await checkUpdates()
|
|
389
|
+
this.current = report
|
|
390
|
+
this.emit()
|
|
391
|
+
return report
|
|
392
|
+
} catch {
|
|
393
|
+
return this.current
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** Mark the set as installing (set by the modal before the restart). */
|
|
398
|
+
setInstalling(flag: boolean): void {
|
|
399
|
+
this.installingFlag = flag
|
|
400
|
+
this.emit()
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Idle refresh while mounted (60 s) — no overlapping poll loop. */
|
|
404
|
+
startIdleRefresh(): void {
|
|
405
|
+
if (this.refreshTimer !== null) return
|
|
406
|
+
this.refreshTimer = window.setInterval(() => {
|
|
407
|
+
void this.refresh()
|
|
408
|
+
}, 60_000) as unknown as number
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
stop(): void {
|
|
412
|
+
if (this.refreshTimer !== null) {
|
|
413
|
+
window.clearInterval(this.refreshTimer)
|
|
414
|
+
this.refreshTimer = null
|
|
415
|
+
}
|
|
416
|
+
this.listeners.clear()
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Client cordis inject: no service dependencies — the plugin reads the
|
|
422
|
+
* hq-edge report purely through same-origin fetch (forwarded by its own node
|
|
423
|
+
* half). `inject` stays empty so the loader never waits on another service.
|
|
424
|
+
*/
|
|
425
|
+
export const inject: string[] = []
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Client plugin body — mount the sidebar toolbutton + review modal.
|
|
429
|
+
* @param ctx - real cordis client context.
|
|
430
|
+
* @returns disposer removing the row and all subscriptions.
|
|
431
|
+
*/
|
|
432
|
+
export function apply(ctx: ClientContext): () => void {
|
|
433
|
+
if (typeof document === 'undefined') return () => {}
|
|
434
|
+
injectUpdateStyles()
|
|
435
|
+
|
|
436
|
+
const store = new UpdateStore()
|
|
437
|
+
const copy = { zh: COPY.zh, en: COPY.en }
|
|
438
|
+
const copyFor = (): typeof COPY.zh => (locale === 'en' ? copy.en : copy.zh)
|
|
439
|
+
|
|
440
|
+
// Locale: re-render the row copy when <html lang> changes. The sidebar
|
|
441
|
+
// helper subscribes via `refresh.subscribe`; we fan out to its listener.
|
|
442
|
+
let locale: HostLocale | undefined = detectHostLocale()
|
|
443
|
+
const localeSubscribers = new Set<() => void>()
|
|
444
|
+
const refreshSubscribe = (listener: () => void): (() => void) => {
|
|
445
|
+
localeSubscribers.add(listener)
|
|
446
|
+
return () => {
|
|
447
|
+
localeSubscribers.delete(listener)
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const unsubscribeLocale = observeHostLocale((next) => {
|
|
451
|
+
locale = next
|
|
452
|
+
for (const fn of [...localeSubscribers]) {
|
|
453
|
+
try {
|
|
454
|
+
fn()
|
|
455
|
+
} catch {
|
|
456
|
+
/* one bad subscriber must not strand the rest */
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
})
|
|
460
|
+
const labelState = { label: entryCopy(locale).label, tooltip: entryCopy(locale).tooltip }
|
|
461
|
+
const applyLabel = (): void => {
|
|
462
|
+
const c = entryCopy(locale)
|
|
463
|
+
labelState.label = c.label
|
|
464
|
+
labelState.tooltip = c.tooltip
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const openView = (): void => {
|
|
468
|
+
// Always re-check on open so the review screen never shows stale data.
|
|
469
|
+
void store.check().then((report) => {
|
|
470
|
+
if (report !== null) openUpdateView(store, copyFor())
|
|
471
|
+
})
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const disposeEntry = mountSidebarEntry({
|
|
475
|
+
rowAttribute: UPDATE_ENTRY_ATTRIBUTE,
|
|
476
|
+
rowSelector: UPDATE_ENTRY_SELECTOR,
|
|
477
|
+
plugin: PLUGIN_ID,
|
|
478
|
+
icon: UPDATE_ICON,
|
|
479
|
+
css: ENTRY_CSS,
|
|
480
|
+
label: () => labelState.label,
|
|
481
|
+
tooltip: () => labelState.tooltip,
|
|
482
|
+
refresh: { subscribe: refreshSubscribe },
|
|
483
|
+
badge: {
|
|
484
|
+
subscribe: (listener) => store.subscribe(listener),
|
|
485
|
+
count: () => store.badgeCount(),
|
|
486
|
+
},
|
|
487
|
+
onToggle: openView,
|
|
488
|
+
position: 'after',
|
|
489
|
+
familySelectors: HUAQIU_SIDEBAR_ENTRY_FAMILY,
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
store.startIdleRefresh()
|
|
493
|
+
void store.refresh()
|
|
494
|
+
|
|
495
|
+
const dispose = (): void => {
|
|
496
|
+
try {
|
|
497
|
+
disposeEntry()
|
|
498
|
+
} catch {
|
|
499
|
+
/* already disposed */
|
|
500
|
+
}
|
|
501
|
+
unsubscribeLocale()
|
|
502
|
+
store.stop()
|
|
503
|
+
}
|
|
504
|
+
if (typeof ctx.effect === 'function') {
|
|
505
|
+
ctx.effect(dispose, 'plugin-update')
|
|
506
|
+
}
|
|
507
|
+
return dispose
|
|
508
|
+
}
|