perfectionist-plugins 2.4.7 → 2.5.0
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 +52 -52
- package/auto-accept.js +296 -294
- package/auto-claimer.js +1 -37
- package/auto-read.js +107 -42
- package/ban-blocker.js +483 -677
- package/better-client.js +362 -793
- package/dodge-helper.js +375 -673
- package/hide-unowned-skins.js +63 -686
- package/package.json +40 -40
- package/perfectionist-utils.js +540 -301
- package/unlock-profile-skins.js +174 -620
package/ban-blocker.js
CHANGED
|
@@ -1,677 +1,483 @@
|
|
|
1
|
-
export const version = "
|
|
2
|
-
|
|
3
|
-
const PLUGIN_ID = "ban-blocker";
|
|
4
|
-
const STYLE_ID = "perfectionist-ban-blocker-style";
|
|
5
|
-
const
|
|
6
|
-
const SESSION_URI = "/lol-champ-select/v1/session";
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"<
|
|
21
|
-
"<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
top: -2px !important;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
let
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if (
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
if (
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
continue;
|
|
485
|
-
}
|
|
486
|
-
if (nativeSwapIn(r.circle)) swapSeen.add(key);
|
|
487
|
-
if (!swapSeen.has(key)) {
|
|
488
|
-
if (btn) {
|
|
489
|
-
btn.remove();
|
|
490
|
-
buttons.delete(r.index);
|
|
491
|
-
}
|
|
492
|
-
continue;
|
|
493
|
-
}
|
|
494
|
-
if (!btn || !btn.isConnected || btn.parentElement !== r.circle) {
|
|
495
|
-
if (btn) btn.remove();
|
|
496
|
-
for (const orphan of r.circle.querySelectorAll(`:scope > .${BTN_CLASS}`)) orphan.remove();
|
|
497
|
-
btn = buildBadge(r.index);
|
|
498
|
-
r.circle.appendChild(btn);
|
|
499
|
-
buttons.set(r.index, btn);
|
|
500
|
-
}
|
|
501
|
-
btn.dataset.slot = String(r.index);
|
|
502
|
-
const cell = r.cell ?? r.slotId;
|
|
503
|
-
btn.dataset.cell = cell == null ? "" : String(cell);
|
|
504
|
-
}
|
|
505
|
-
for (const [slot, btn] of [...buttons]) {
|
|
506
|
-
if (!list[slot] || !btn.isConnected) {
|
|
507
|
-
btn.remove();
|
|
508
|
-
buttons.delete(slot);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
function renderButtons() {
|
|
514
|
-
const hasSession = !!lastSession?.myTeam;
|
|
515
|
-
for (const [, btn] of buttons) {
|
|
516
|
-
const id = playerForBadge(btn);
|
|
517
|
-
const on = id != null && isBlocked(id);
|
|
518
|
-
btn.classList.toggle("on", on);
|
|
519
|
-
btn.classList.toggle("unresolved", id == null && hasSession);
|
|
520
|
-
const title = on ? "Blocked — click to allow their bans" : "Block this teammate's bans";
|
|
521
|
-
if (btn.title !== title) btn.title = title;
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
function removeButtons() {
|
|
526
|
-
for (const btn of buttons.values()) btn.remove();
|
|
527
|
-
buttons.clear();
|
|
528
|
-
for (const el of document.querySelectorAll(`.${BTN_CLASS}[data-bb]`)) el.remove();
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function queueRefresh() {
|
|
532
|
-
clearTimeout(refreshTimer);
|
|
533
|
-
refreshTimer = setTimeout(poll, 80);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function scheduleUi() {
|
|
537
|
-
if (uiScheduled) return;
|
|
538
|
-
uiScheduled = true;
|
|
539
|
-
queueMicrotask(() => {
|
|
540
|
-
uiScheduled = false;
|
|
541
|
-
try {
|
|
542
|
-
update();
|
|
543
|
-
} catch (err) {
|
|
544
|
-
logError("ui update failed.", err);
|
|
545
|
-
}
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
function update() {
|
|
550
|
-
if (goneAway) return;
|
|
551
|
-
const s = lastSession;
|
|
552
|
-
if (s?.myTeam) bindRowsToTeam(s);
|
|
553
|
-
const visible = !s?.myTeam ? true : banPending(s);
|
|
554
|
-
if (visible) {
|
|
555
|
-
uiVisible = true;
|
|
556
|
-
ensureButtons();
|
|
557
|
-
renderButtons();
|
|
558
|
-
} else if (uiVisible) {
|
|
559
|
-
uiVisible = false;
|
|
560
|
-
swapSeen.clear();
|
|
561
|
-
removeButtons();
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
async function poll() {
|
|
566
|
-
const get = api?.lcu?.get;
|
|
567
|
-
if (!get || pollInFlight) return;
|
|
568
|
-
if (goneAway && !document.querySelector(".summoner-object")) return;
|
|
569
|
-
pollInFlight = true;
|
|
570
|
-
try {
|
|
571
|
-
onSession(await get(SESSION_URI));
|
|
572
|
-
} catch {
|
|
573
|
-
onSessionGone();
|
|
574
|
-
} finally {
|
|
575
|
-
pollInFlight = false;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
function onEnable(utils) {
|
|
580
|
-
api = utils;
|
|
581
|
-
try {
|
|
582
|
-
removeStyle = api.addStyle(STYLE_ID, CSS);
|
|
583
|
-
} catch (err) {
|
|
584
|
-
logError("style setup failed.", err);
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
pollTimer = setInterval(poll, POLL_MS);
|
|
588
|
-
ensureTimer = setInterval(() => {
|
|
589
|
-
if (uiVisible) update();
|
|
590
|
-
}, ENSURE_MS);
|
|
591
|
-
|
|
592
|
-
try {
|
|
593
|
-
observer = new MutationObserver(() => scheduleUi());
|
|
594
|
-
observer.observe(document.body, { childList: true, subtree: true });
|
|
595
|
-
} catch (err) {
|
|
596
|
-
logError("observer setup failed.", err);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
const onPush = (msg) => onSession(msg);
|
|
600
|
-
let offs = [];
|
|
601
|
-
for (const uri of [SESSION_URI, `${SESSION_URI}/timer`]) {
|
|
602
|
-
try {
|
|
603
|
-
const off = api.subscribe(uri, onPush);
|
|
604
|
-
if (off) offs.push(off);
|
|
605
|
-
} catch (err) {
|
|
606
|
-
logError(`subscribe failed for ${uri}.`, err);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
unsubscribe = () => {
|
|
610
|
-
for (const off of offs) {
|
|
611
|
-
try {
|
|
612
|
-
off();
|
|
613
|
-
} catch (err) {
|
|
614
|
-
logError("unsubscribe failed.", err);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
};
|
|
618
|
-
|
|
619
|
-
poll();
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
function onDisable() {
|
|
623
|
-
unsubscribe?.();
|
|
624
|
-
unsubscribe = null;
|
|
625
|
-
clearInterval(pollTimer);
|
|
626
|
-
clearInterval(ensureTimer);
|
|
627
|
-
clearTimeout(refreshTimer);
|
|
628
|
-
clearTimeout(pumpTimer);
|
|
629
|
-
clearTimeout(goneTimer);
|
|
630
|
-
pollTimer = ensureTimer = refreshTimer = pumpTimer = goneTimer = null;
|
|
631
|
-
observer?.disconnect();
|
|
632
|
-
observer = null;
|
|
633
|
-
|
|
634
|
-
uiVisible = false;
|
|
635
|
-
swapSeen.clear();
|
|
636
|
-
goneAway = true;
|
|
637
|
-
target = undefined;
|
|
638
|
-
sent = 0;
|
|
639
|
-
failedUntil.clear();
|
|
640
|
-
failCount.clear();
|
|
641
|
-
cellToPlayer.clear();
|
|
642
|
-
enabled.clear();
|
|
643
|
-
playerNames.clear();
|
|
644
|
-
alias.clear();
|
|
645
|
-
fetchedNames.clear();
|
|
646
|
-
lastSession = null;
|
|
647
|
-
rows = [];
|
|
648
|
-
lastDetect = -Infinity;
|
|
649
|
-
removeButtons();
|
|
650
|
-
|
|
651
|
-
removeStyle?.();
|
|
652
|
-
removeStyle = null;
|
|
653
|
-
api = null;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
export function init() {
|
|
657
|
-
const registerWith = (utils) => {
|
|
658
|
-
utils.registerPlugin({
|
|
659
|
-
id: PLUGIN_ID,
|
|
660
|
-
name: "Ban Blocker",
|
|
661
|
-
description: "During bans, blocking a teammate instantly claims the champion they hover to ban.",
|
|
662
|
-
version,
|
|
663
|
-
onEnable,
|
|
664
|
-
onDisable,
|
|
665
|
-
});
|
|
666
|
-
};
|
|
667
|
-
|
|
668
|
-
if (window.PerfectionistUtils) {
|
|
669
|
-
registerWith(window.PerfectionistUtils);
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
672
|
-
addEventListener(
|
|
673
|
-
"perfectionist:utils-ready",
|
|
674
|
-
(e) => registerWith(e.detail ?? window.PerfectionistUtils),
|
|
675
|
-
{ once: true },
|
|
676
|
-
);
|
|
677
|
-
}
|
|
1
|
+
export const version = "4.0.0";
|
|
2
|
+
|
|
3
|
+
const PLUGIN_ID = "ban-blocker";
|
|
4
|
+
const STYLE_ID = "perfectionist-ban-blocker-style";
|
|
5
|
+
const BADGE_CLASS = "bb-toggle";
|
|
6
|
+
const SESSION_URI = "/lol-champ-select/v1/session";
|
|
7
|
+
const ACTIONS_URI = "/lol-champ-select/v1/session/actions";
|
|
8
|
+
const PICKABLE_URI = "/lol-champ-select/v1/pickable-champion-ids";
|
|
9
|
+
const CHAMPION_SUMMARY_URI = "/lol-game-data/assets/v1/champion-summary.json";
|
|
10
|
+
const ROWS_SELECTOR = ".summoner-array.your-party";
|
|
11
|
+
|
|
12
|
+
// A PATCH that the session has not reflected yet is re-sent after this long (the server can drop an intent
|
|
13
|
+
// that lands while the session is mid-update). Kept short: a teammate can lock a ban in a fraction of a second.
|
|
14
|
+
const CONFIRM_MS = 220;
|
|
15
|
+
const MAX_SENDS = 4;
|
|
16
|
+
// Safety net only: the session WebSocket push is the fast path. Polling runs only while a block is active.
|
|
17
|
+
const POLL_MS = 1000;
|
|
18
|
+
|
|
19
|
+
const BAN_GLYPH_SVG =
|
|
20
|
+
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'>" +
|
|
21
|
+
"<g fill='none' stroke='white' stroke-width='2.4'>" +
|
|
22
|
+
"<circle cx='10' cy='10' r='7'/><path d='M5.1 14.9 14.9 5.1'/></g></svg>";
|
|
23
|
+
const BAN_GLYPH = `data:image/svg+xml,${encodeURIComponent(BAN_GLYPH_SVG)}`;
|
|
24
|
+
|
|
25
|
+
const CSS = `
|
|
26
|
+
.swap-button-component.${BADGE_CLASS} {
|
|
27
|
+
position: absolute !important; top: -2px !important; right: -2px !important; left: auto !important; bottom: auto !important;
|
|
28
|
+
z-index: 50 !important; pointer-events: auto !important;
|
|
29
|
+
}
|
|
30
|
+
.swap-button-component.${BADGE_CLASS} .icon {
|
|
31
|
+
-webkit-mask-image: url("${BAN_GLYPH}") !important; mask-image: url("${BAN_GLYPH}") !important;
|
|
32
|
+
-webkit-mask-size: 70% 70% !important; mask-size: 70% 70% !important;
|
|
33
|
+
}
|
|
34
|
+
.swap-button-component.${BADGE_CLASS}.on .frame-color { background-color: rgb(200, 170, 110) !important; }
|
|
35
|
+
.swap-button-component.${BADGE_CLASS}.on .icon { background-color: rgb(1, 10, 19) !important; }
|
|
36
|
+
.swap-button-component.${BADGE_CLASS}.on .frame-color:hover { background-color: rgb(240, 230, 210) !important; }
|
|
37
|
+
.swap-button-component.${BADGE_CLASS}.on.warn .frame-color { background-color: rgb(190, 30, 55) !important; }
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
let api = null;
|
|
41
|
+
let active = false;
|
|
42
|
+
let removeStyle = null;
|
|
43
|
+
let unsubscribe = null;
|
|
44
|
+
let unwatchRows = null;
|
|
45
|
+
let removeFrameHook = null;
|
|
46
|
+
let pollTimer = null;
|
|
47
|
+
let confirmTimer = null;
|
|
48
|
+
|
|
49
|
+
let session = null;
|
|
50
|
+
let sessionKey = null;
|
|
51
|
+
let pickable = null;
|
|
52
|
+
let pickableLoading = false;
|
|
53
|
+
let championNames = null;
|
|
54
|
+
|
|
55
|
+
// Blocks follow the player, not the seat: swapping positions or pick order moves players between cells.
|
|
56
|
+
const blocked = new Set();
|
|
57
|
+
const cellPlayer = new Map();
|
|
58
|
+
const hoverSince = new Map();
|
|
59
|
+
const unprotectable = new Map();
|
|
60
|
+
const refusedUntil = new Map();
|
|
61
|
+
const REFUSED_MS = 1500;
|
|
62
|
+
const badges = new Map();
|
|
63
|
+
|
|
64
|
+
// What we last asked the server for, and what the user had declared before we took over.
|
|
65
|
+
let sent = { actionId: null, championId: 0, at: 0, count: 0 };
|
|
66
|
+
let userIntent = null;
|
|
67
|
+
let ownsIntent = false;
|
|
68
|
+
let restoreDone = false;
|
|
69
|
+
|
|
70
|
+
function logError(scope, error) {
|
|
71
|
+
console.error(`[Ban Blocker] ${scope}`, error);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function flatActions(s) {
|
|
75
|
+
const out = [];
|
|
76
|
+
for (const group of Array.isArray(s?.actions) ? s.actions : []) {
|
|
77
|
+
if (Array.isArray(group)) out.push(...group);
|
|
78
|
+
else if (group) out.push(group);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function myPickAction(s, actions) {
|
|
84
|
+
const me = s?.localPlayerCellId;
|
|
85
|
+
return actions.find((a) => a.type === "pick" && a.actorCellId === me && !a.completed) ?? null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function banPhaseOpen(s, actions) {
|
|
89
|
+
return actions.some((a) => a.type === "ban" && !a.completed);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function playerKey(p) {
|
|
93
|
+
return p?.puuid || p?.obfuscatedPuuid || (p?.summonerId ? `s:${p.summonerId}` : "") || p?.obfuscatedSummonerId || `cell:${p?.cellId}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isBlockedCell(cell) {
|
|
97
|
+
const key = cellPlayer.get(cell);
|
|
98
|
+
return key !== undefined && blocked.has(key);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function takenByAlly(s, championId) {
|
|
102
|
+
return (s?.myTeam ?? []).some((p) => p.championId === championId && p.cellId !== s.localPlayerCellId);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* ---------------------------------------------------------------------------------------------
|
|
106
|
+
* Decision engine: runs synchronously on every session push and fires the PATCH in the same tick.
|
|
107
|
+
* ------------------------------------------------------------------------------------------- */
|
|
108
|
+
|
|
109
|
+
function pickThreat(s, actions) {
|
|
110
|
+
const me = s.localPlayerCellId;
|
|
111
|
+
const now = performance.now();
|
|
112
|
+
let best = null;
|
|
113
|
+
for (const a of actions) {
|
|
114
|
+
if (a.type !== "ban" || a.completed || a.actorCellId === me || !isBlockedCell(a.actorCellId)) continue;
|
|
115
|
+
const champ = a.championId > 0 ? a.championId : 0;
|
|
116
|
+
const prev = hoverSince.get(a.actorCellId);
|
|
117
|
+
if (!prev || prev.championId !== champ) hoverSince.set(a.actorCellId, { championId: champ, at: now });
|
|
118
|
+
if (!champ || takenByAlly(s, champ)) continue;
|
|
119
|
+
if ((pickable && !pickable.has(champ)) || (refusedUntil.get(champ) ?? 0) > now) {
|
|
120
|
+
unprotectable.set(a.actorCellId, champ);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
unprotectable.delete(a.actorCellId);
|
|
124
|
+
const since = hoverSince.get(a.actorCellId).at;
|
|
125
|
+
const candidate = { championId: champ, cell: a.actorCellId, inProgress: a.isInProgress === true, since };
|
|
126
|
+
if (!best) {
|
|
127
|
+
best = candidate;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
// Prefer bans that are live right now, then keep the champion we already hold (no flip-flopping),
|
|
131
|
+
// then the most recent hover (the one most likely to be locked next).
|
|
132
|
+
const score = (c) => (c.inProgress ? 4 : 0) + (c.championId === sent.championId ? 2 : 0);
|
|
133
|
+
const diff = score(candidate) - score(best);
|
|
134
|
+
if (diff > 0 || (diff === 0 && candidate.since > best.since)) best = candidate;
|
|
135
|
+
}
|
|
136
|
+
return best;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function patch(actionId, championId) {
|
|
140
|
+
sent = {
|
|
141
|
+
actionId,
|
|
142
|
+
championId,
|
|
143
|
+
at: performance.now(),
|
|
144
|
+
count: sent.actionId === actionId && sent.championId === championId ? sent.count + 1 : 1,
|
|
145
|
+
};
|
|
146
|
+
fetch(`${ACTIONS_URI}/${actionId}`, {
|
|
147
|
+
method: "PATCH",
|
|
148
|
+
headers: { "Content-Type": "application/json" },
|
|
149
|
+
body: JSON.stringify({ championId }),
|
|
150
|
+
})
|
|
151
|
+
.then((response) => {
|
|
152
|
+
if (response.ok || !active) return;
|
|
153
|
+
// The server refused this champion for us (not owned, disabled, mid-update...). Back off briefly and
|
|
154
|
+
// let the next evaluation protect another hovered champion if there is one.
|
|
155
|
+
refusedUntil.set(championId, performance.now() + REFUSED_MS);
|
|
156
|
+
if (session) evaluate(session);
|
|
157
|
+
renderBadges();
|
|
158
|
+
})
|
|
159
|
+
.catch(() => {});
|
|
160
|
+
scheduleConfirm();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function scheduleConfirm() {
|
|
164
|
+
clearTimeout(confirmTimer);
|
|
165
|
+
confirmTimer = setTimeout(() => {
|
|
166
|
+
confirmTimer = null;
|
|
167
|
+
if (session) evaluate(session);
|
|
168
|
+
}, CONFIRM_MS);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function evaluate(s) {
|
|
172
|
+
const actions = flatActions(s);
|
|
173
|
+
const myPick = myPickAction(s, actions);
|
|
174
|
+
if (!myPick || myPick.isInProgress) return;
|
|
175
|
+
const current = myPick.championId > 0 ? myPick.championId : 0;
|
|
176
|
+
|
|
177
|
+
const threat = blocked.size ? pickThreat(s, actions) : null;
|
|
178
|
+
if (threat) {
|
|
179
|
+
if (!ownsIntent) {
|
|
180
|
+
ownsIntent = true;
|
|
181
|
+
restoreDone = false;
|
|
182
|
+
userIntent = current;
|
|
183
|
+
}
|
|
184
|
+
if (current === threat.championId) return;
|
|
185
|
+
const resend = sent.actionId === myPick.id && sent.championId === threat.championId;
|
|
186
|
+
if (resend && (sent.count >= MAX_SENDS || performance.now() - sent.at < CONFIRM_MS)) return;
|
|
187
|
+
patch(myPick.id, threat.championId);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Nothing to protect any more. Once the blocked teammates are done banning, give the user back the pick
|
|
192
|
+
// they had declared before we stepped in.
|
|
193
|
+
if (!ownsIntent || restoreDone) return;
|
|
194
|
+
const blockedBansLeft = actions.some((a) => a.type === "ban" && !a.completed && isBlockedCell(a.actorCellId));
|
|
195
|
+
if (blockedBansLeft && blocked.size) return;
|
|
196
|
+
restoreDone = true;
|
|
197
|
+
ownsIntent = false;
|
|
198
|
+
if (current === sent.championId && userIntent !== null && userIntent !== current) patch(myPick.id, userIntent);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* ---------------------------------------------------------------------------------------------
|
|
202
|
+
* Session handling
|
|
203
|
+
* ------------------------------------------------------------------------------------------- */
|
|
204
|
+
|
|
205
|
+
// Identifies one champ select. It must not change when players swap seats, or the blocks would be lost.
|
|
206
|
+
function keyOf(s) {
|
|
207
|
+
if (Number(s?.gameId) > 0) return `game:${s.gameId}`;
|
|
208
|
+
return `team:${(s?.myTeam ?? []).map(playerKey).sort().join(",")}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function resetSession() {
|
|
212
|
+
blocked.clear();
|
|
213
|
+
cellPlayer.clear();
|
|
214
|
+
hoverSince.clear();
|
|
215
|
+
unprotectable.clear();
|
|
216
|
+
refusedUntil.clear();
|
|
217
|
+
sent = { actionId: null, championId: 0, at: 0, count: 0 };
|
|
218
|
+
userIntent = null;
|
|
219
|
+
ownsIntent = false;
|
|
220
|
+
restoreDone = false;
|
|
221
|
+
pickable = null;
|
|
222
|
+
session = null;
|
|
223
|
+
sessionKey = null;
|
|
224
|
+
clearTimeout(confirmTimer);
|
|
225
|
+
confirmTimer = null;
|
|
226
|
+
syncPolling();
|
|
227
|
+
removeBadges();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function loadPickable() {
|
|
231
|
+
if (pickableLoading || !api) return;
|
|
232
|
+
pickableLoading = true;
|
|
233
|
+
api.lcu
|
|
234
|
+
.get(PICKABLE_URI)
|
|
235
|
+
.then((ids) => {
|
|
236
|
+
if (active && Array.isArray(ids) && ids.length) pickable = new Set(ids);
|
|
237
|
+
})
|
|
238
|
+
.catch(() => {})
|
|
239
|
+
.finally(() => {
|
|
240
|
+
pickableLoading = false;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function applySession(s) {
|
|
245
|
+
if (!s || !Array.isArray(s.myTeam)) return;
|
|
246
|
+
const key = keyOf(s);
|
|
247
|
+
if (key !== sessionKey) {
|
|
248
|
+
resetSession();
|
|
249
|
+
sessionKey = key;
|
|
250
|
+
}
|
|
251
|
+
session = s;
|
|
252
|
+
cellPlayer.clear();
|
|
253
|
+
for (const member of s.myTeam) cellPlayer.set(member.cellId, playerKey(member));
|
|
254
|
+
if (!pickable) loadPickable();
|
|
255
|
+
try {
|
|
256
|
+
evaluate(s);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
logError("decision failed.", error);
|
|
259
|
+
}
|
|
260
|
+
syncPolling();
|
|
261
|
+
renderBadges();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function onSessionPush(message) {
|
|
265
|
+
if (!active) return;
|
|
266
|
+
if (message?.eventType === "Delete") {
|
|
267
|
+
resetSession();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
applySession(message?.data ?? message);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function syncPolling() {
|
|
274
|
+
const wanted = active && session && blocked.size > 0 && banPhaseOpen(session, flatActions(session));
|
|
275
|
+
if (wanted && !pollTimer) {
|
|
276
|
+
pollTimer = setInterval(() => {
|
|
277
|
+
api?.lcu
|
|
278
|
+
.get(SESSION_URI)
|
|
279
|
+
.then((s) => applySession(s))
|
|
280
|
+
.catch(() => {});
|
|
281
|
+
}, POLL_MS);
|
|
282
|
+
} else if (!wanted && pollTimer) {
|
|
283
|
+
clearInterval(pollTimer);
|
|
284
|
+
pollTimer = null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/* ---------------------------------------------------------------------------------------------
|
|
289
|
+
* UI: one BLOCK badge on every teammate's champion icon while bans are still open.
|
|
290
|
+
* ------------------------------------------------------------------------------------------- */
|
|
291
|
+
|
|
292
|
+
// Ally rows render in myTeam order (placeholder rows are "removed"). The local player's row (.is-self)
|
|
293
|
+
// confirms the alignment; slot ids are the fallback for layouts where the order differs.
|
|
294
|
+
function allyRows(s) {
|
|
295
|
+
const party = document.querySelector(ROWS_SELECTOR);
|
|
296
|
+
if (!party) return [];
|
|
297
|
+
const rows = [...party.querySelectorAll(".summoner-object")].filter(
|
|
298
|
+
(row) => !row.closest(".summoner-wrapper")?.classList.contains("removed"),
|
|
299
|
+
);
|
|
300
|
+
const team = s?.myTeam ?? [];
|
|
301
|
+
if (!rows.length || !team.length) return [];
|
|
302
|
+
const selfRow = rows.findIndex((row) => row.classList.contains("is-self"));
|
|
303
|
+
const selfCell = team.findIndex((p) => p.cellId === s.localPlayerCellId);
|
|
304
|
+
if (rows.length === team.length && (selfRow < 0 || selfRow === selfCell)) {
|
|
305
|
+
return rows.map((row, index) => ({ row, cell: team[index].cellId }));
|
|
306
|
+
}
|
|
307
|
+
const cells = new Set(team.map((p) => p.cellId));
|
|
308
|
+
const offset = team.length && Math.min(...team.map((p) => p.cellId)) >= 5 ? 5 : 0;
|
|
309
|
+
return rows
|
|
310
|
+
.map((row) => {
|
|
311
|
+
const slot = /slot-id-(\d+)/.exec(row.className);
|
|
312
|
+
if (!slot) return null;
|
|
313
|
+
const id = Number(slot[1]);
|
|
314
|
+
const cell = cells.has(id) ? id : cells.has(id + offset) ? id + offset : null;
|
|
315
|
+
return cell === null ? null : { row, cell };
|
|
316
|
+
})
|
|
317
|
+
.filter(Boolean);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function championName(id) {
|
|
321
|
+
if (!championNames && api) {
|
|
322
|
+
championNames = new Map();
|
|
323
|
+
api.lcu
|
|
324
|
+
.get(CHAMPION_SUMMARY_URI, { ttl: 60 * 60 * 1000 })
|
|
325
|
+
.then((list) => {
|
|
326
|
+
for (const c of Array.isArray(list) ? list : []) championNames.set(c.id, c.name);
|
|
327
|
+
renderBadges();
|
|
328
|
+
})
|
|
329
|
+
.catch(() => {
|
|
330
|
+
championNames = null;
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
return championNames?.get(id) ?? "that champion";
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function badgeTitle(cell) {
|
|
337
|
+
if (!isBlockedCell(cell)) return "Block this teammate's bans";
|
|
338
|
+
const champ = unprotectable.get(cell);
|
|
339
|
+
if (champ) return `Blocked, but ${championName(champ)} can't be declared as your pick (not available to you), so this ban can't be stopped.`;
|
|
340
|
+
return "Blocked: the champion they hover to ban is declared as your pick. Click to allow their bans.";
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function buildBadge(cell) {
|
|
344
|
+
const badge = document.createElement("div");
|
|
345
|
+
badge.className = `swap-button-component ${BADGE_CLASS}`;
|
|
346
|
+
badge.dataset.cell = String(cell);
|
|
347
|
+
badge.innerHTML =
|
|
348
|
+
'<div class="swap-button--btn uikit-framed-icon"><div class="outer-mask interactive">' +
|
|
349
|
+
'<div class="frame-color" style="padding: 2px;"><div class="icon"></div></div></div></div>';
|
|
350
|
+
const stop = (event) => {
|
|
351
|
+
event.stopPropagation();
|
|
352
|
+
event.preventDefault();
|
|
353
|
+
};
|
|
354
|
+
for (const type of ["mouseenter", "mouseleave", "mousedown", "mouseup", "pointerdown", "pointerup"]) badge.addEventListener(type, stop);
|
|
355
|
+
badge.addEventListener("click", (event) => {
|
|
356
|
+
stop(event);
|
|
357
|
+
toggle(Number(badge.dataset.cell));
|
|
358
|
+
});
|
|
359
|
+
return badge;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function toggle(cell) {
|
|
363
|
+
if (!session || cell === session.localPlayerCellId) return;
|
|
364
|
+
const key = cellPlayer.get(cell);
|
|
365
|
+
if (key === undefined) return;
|
|
366
|
+
if (blocked.has(key)) {
|
|
367
|
+
blocked.delete(key);
|
|
368
|
+
unprotectable.delete(cell);
|
|
369
|
+
} else {
|
|
370
|
+
blocked.add(key);
|
|
371
|
+
}
|
|
372
|
+
evaluate(session);
|
|
373
|
+
syncPolling();
|
|
374
|
+
renderBadges();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function removeBadges() {
|
|
378
|
+
for (const badge of badges.values()) badge.remove();
|
|
379
|
+
badges.clear();
|
|
380
|
+
for (const stray of document.querySelectorAll(`.${BADGE_CLASS}`)) stray.remove();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function renderBadges() {
|
|
384
|
+
if (!active) return;
|
|
385
|
+
const s = session;
|
|
386
|
+
if (!s || !banPhaseOpen(s, flatActions(s))) {
|
|
387
|
+
if (badges.size) removeBadges();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const seen = new Set();
|
|
391
|
+
for (const { row, cell } of allyRows(s)) {
|
|
392
|
+
if (cell === s.localPlayerCellId) continue;
|
|
393
|
+
const host = row.querySelector(".champion-icon-container");
|
|
394
|
+
if (!host) continue;
|
|
395
|
+
let badge = badges.get(cell);
|
|
396
|
+
if (!badge || badge.parentElement !== host) {
|
|
397
|
+
badge?.remove();
|
|
398
|
+
badge = buildBadge(cell);
|
|
399
|
+
host.appendChild(badge);
|
|
400
|
+
badges.set(cell, badge);
|
|
401
|
+
}
|
|
402
|
+
seen.add(cell);
|
|
403
|
+
const on = isBlockedCell(cell);
|
|
404
|
+
badge.classList.toggle("on", on);
|
|
405
|
+
badge.classList.toggle("warn", on && unprotectable.has(cell));
|
|
406
|
+
const title = badgeTitle(cell);
|
|
407
|
+
if (badge.title !== title) badge.title = title;
|
|
408
|
+
}
|
|
409
|
+
for (const [cell, badge] of badges) {
|
|
410
|
+
if (!seen.has(cell)) {
|
|
411
|
+
badge.remove();
|
|
412
|
+
badges.delete(cell);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function onFrame() {
|
|
418
|
+
// Ember re-renders rows freely; re-attach badges only when one went missing.
|
|
419
|
+
if (!session) return;
|
|
420
|
+
for (const badge of badges.values()) {
|
|
421
|
+
if (!badge.isConnected) {
|
|
422
|
+
renderBadges();
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (badges.size === 0 && document.querySelector(ROWS_SELECTOR)) renderBadges();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/* ------------------------------------------------------------------------------------------- */
|
|
430
|
+
|
|
431
|
+
function onEnable(utils) {
|
|
432
|
+
api = utils;
|
|
433
|
+
active = true;
|
|
434
|
+
removeStyle = utils.addStyle(STYLE_ID, CSS);
|
|
435
|
+
unsubscribe = utils.subscribe(SESSION_URI, onSessionPush);
|
|
436
|
+
unwatchRows = utils.watch(ROWS_SELECTOR, () => {
|
|
437
|
+
if (!session) {
|
|
438
|
+
utils.lcu
|
|
439
|
+
.get(SESSION_URI)
|
|
440
|
+
.then((s) => applySession(s))
|
|
441
|
+
.catch(() => {});
|
|
442
|
+
} else renderBadges();
|
|
443
|
+
});
|
|
444
|
+
removeFrameHook = utils.onFrame ? utils.onFrame(onFrame) : null;
|
|
445
|
+
utils.lcu
|
|
446
|
+
.get(SESSION_URI)
|
|
447
|
+
.then((s) => applySession(s))
|
|
448
|
+
.catch(() => {});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function onDisable() {
|
|
452
|
+
active = false;
|
|
453
|
+
unsubscribe?.();
|
|
454
|
+
unwatchRows?.();
|
|
455
|
+
removeFrameHook?.();
|
|
456
|
+
unsubscribe = unwatchRows = removeFrameHook = null;
|
|
457
|
+
resetSession();
|
|
458
|
+
clearInterval(pollTimer);
|
|
459
|
+
pollTimer = null;
|
|
460
|
+
removeStyle?.();
|
|
461
|
+
removeStyle = null;
|
|
462
|
+
championNames = null;
|
|
463
|
+
api = null;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export function init() {
|
|
467
|
+
const registerWith = (utils) => {
|
|
468
|
+
utils.registerPlugin({
|
|
469
|
+
id: PLUGIN_ID,
|
|
470
|
+
name: "Ban Blocker",
|
|
471
|
+
description: "During bans, blocking a teammate instantly claims the champion they hover to ban.",
|
|
472
|
+
version,
|
|
473
|
+
onEnable,
|
|
474
|
+
onDisable,
|
|
475
|
+
});
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
if (window.PerfectionistUtils) {
|
|
479
|
+
registerWith(window.PerfectionistUtils);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
addEventListener("perfectionist:utils-ready", (event) => registerWith(event.detail ?? window.PerfectionistUtils), { once: true });
|
|
483
|
+
}
|